Skip to content

Commit 391e859

Browse files
authored
Merge pull request #19 from bugst/templated_resolver
Generics-based resolver
2 parents e8b9735 + ae9c7ba commit 391e859

File tree

2 files changed

+116
-82
lines changed

2 files changed

+116
-82
lines changed

resolver.go

Lines changed: 91 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,21 @@ type Dependency interface {
1515
}
1616

1717
// Release represents a release, it must provide methods to return Name, Version and Dependencies
18-
type Release interface {
18+
type Release[D Dependency] interface {
1919
GetName() string
2020
GetVersion() *Version
21-
GetDependencies() []Dependency
21+
GetDependencies() []D
2222
}
2323

24-
func match(r Release, dep Dependency) bool {
25-
return r.GetName() == dep.GetName() && dep.GetConstraint().Match(r.GetVersion())
26-
}
27-
28-
// Releases is a list of Release
29-
type Releases []Release
24+
// Releases is a list of Release of the same package (all releases with
25+
// the same Name but different Version)
26+
type Releases[R Release[D], D Dependency] []R
3027

31-
// FilterBy return a subset of the Releases matching the provided Dependency
32-
func (set Releases) FilterBy(dep Dependency) Releases {
33-
res := []Release{}
28+
// FilterBy return a subset of the Releases matching the provided Constraint
29+
func (set Releases[R, D]) FilterBy(c Constraint) Releases[R, D] {
30+
var res Releases[R, D]
3431
for _, r := range set {
35-
if match(r, dep) {
32+
if c.Match(r.GetVersion()) {
3633
res = append(res, r)
3734
}
3835
}
@@ -41,108 +38,138 @@ func (set Releases) FilterBy(dep Dependency) Releases {
4138

4239
// SortDescent sort the Releases in this set in descending order (the lastest
4340
// release is the first)
44-
func (set Releases) SortDescent() {
41+
func (set Releases[R, D]) SortDescent() {
4542
sort.Slice(set, func(i, j int) bool {
4643
return set[i].GetVersion().GreaterThan(set[j].GetVersion())
4744
})
4845
}
4946

50-
// Archive contains all Releases set to consider for dependency resolution
51-
type Archive struct {
52-
Releases map[string]Releases
47+
// Resolver is a container with references to all Releases to consider for
48+
// dependency resolution
49+
type Resolver[R Release[D], D Dependency] struct {
50+
releases map[string]Releases[R, D]
51+
52+
// resolver state
53+
solution map[string]R
54+
depsToProcess []D
55+
problematicDeps map[dependencyHash]int
5356
}
5457

55-
// Resolve will try to depp-resolve dependencies from the Release passed as
56-
// arguent using a backtracking algorithm.
57-
func (ar *Archive) Resolve(release Release) []Release {
58-
mainDep := &bareDependency{
59-
name: release.GetName(),
60-
version: release.GetVersion(),
58+
// NewResolver creates a new archive
59+
func NewResolver[R Release[D], D Dependency]() *Resolver[R, D] {
60+
return &Resolver[R, D]{
61+
releases: map[string]Releases[R, D]{},
6162
}
62-
return ar.resolve(map[string]Release{}, []Dependency{mainDep}, map[Dependency]int{})
6363
}
6464

65-
type bareDependency struct {
66-
name string
67-
version *Version
65+
// AddRelease adds a release to this archive
66+
func (ar *Resolver[R, D]) AddRelease(rel R) {
67+
relName := rel.GetName()
68+
ar.releases[relName] = append(ar.releases[relName], rel)
6869
}
6970

70-
func (b *bareDependency) GetName() string {
71-
return b.name
71+
// AddReleases adds all the releases to this archive
72+
func (ar *Resolver[R, D]) AddReleases(rels ...R) {
73+
for _, rel := range rels {
74+
relName := rel.GetName()
75+
ar.releases[relName] = append(ar.releases[relName], rel)
76+
}
7277
}
7378

74-
func (b *bareDependency) GetConstraint() Constraint {
75-
return &Equals{Version: b.version}
79+
// Resolve will try to depp-resolve dependencies from the Release passed as
80+
// arguent using a backtracking algorithm. This function is NOT thread-safe.
81+
func (ar *Resolver[R, D]) Resolve(release R) Releases[R, D] {
82+
// Initial empty state of the resolver
83+
ar.solution = map[string]R{}
84+
ar.depsToProcess = []D{}
85+
ar.problematicDeps = map[dependencyHash]int{}
86+
87+
// Check if the release is in the archive
88+
if len(ar.releases[release.GetName()].FilterBy(&Equals{Version: release.GetVersion()})) == 0 {
89+
return nil
90+
}
91+
92+
// Add the requested release to the solution and proceed
93+
// with the dependencies resolution
94+
ar.solution[release.GetName()] = release
95+
ar.depsToProcess = append(ar.depsToProcess, release.GetDependencies()...)
96+
return ar.resolve()
7697
}
7798

78-
func (b *bareDependency) String() string {
79-
return b.GetName() + b.GetConstraint().String()
99+
type dependencyHash string
100+
101+
func hashDependency[D Dependency](dep D) dependencyHash {
102+
return dependencyHash(dep.GetName() + "/" + dep.GetConstraint().String())
80103
}
81104

82-
func (ar *Archive) resolve(solution map[string]Release, depsToProcess []Dependency, problematicDeps map[Dependency]int) []Release {
83-
debug("deps to process: %s", depsToProcess)
84-
if len(depsToProcess) == 0 {
105+
func (ar *Resolver[R, D]) resolve() Releases[R, D] {
106+
debug("deps to process: %s", ar.depsToProcess)
107+
if len(ar.depsToProcess) == 0 {
85108
debug("All dependencies have been resolved.")
86-
res := []Release{}
87-
for _, v := range solution {
109+
var res Releases[R, D]
110+
for _, v := range ar.solution {
88111
res = append(res, v)
89112
}
90113
return res
91114
}
92115

93116
// Pick the first dependency in the deps to process
94-
dep := depsToProcess[0]
117+
dep := ar.depsToProcess[0]
95118
depName := dep.GetName()
96119
debug("Considering next dep: %s", depName)
97120

98121
// If a release is already picked in the solution check if it match the dep
99-
if existingRelease, has := solution[depName]; has {
100-
if match(existingRelease, dep) {
122+
if existingRelease, has := ar.solution[depName]; has {
123+
if dep.GetConstraint().Match(existingRelease.GetVersion()) {
101124
debug("%s already in solution and matching", existingRelease)
102-
return ar.resolve(solution, depsToProcess[1:], problematicDeps)
125+
oldDepsToProcess := ar.depsToProcess
126+
ar.depsToProcess = ar.depsToProcess[1:]
127+
if res := ar.resolve(); res != nil {
128+
return res
129+
}
130+
ar.depsToProcess = oldDepsToProcess
131+
return nil
103132
}
104133
debug("%s already in solution do not match... rollingback", existingRelease)
105134
return nil
106135
}
107136

108137
// Otherwise start backtracking the dependency
109-
releases := ar.Releases[dep.GetName()].FilterBy(dep)
138+
releases := ar.releases[depName].FilterBy(dep.GetConstraint())
110139

111140
// Consider the latest versions first
112141
releases.SortDescent()
113-
114-
findMissingDeps := func(deps []Dependency) Dependency {
115-
for _, dep := range deps {
116-
if _, ok := ar.Releases[dep.GetName()]; !ok {
117-
return dep
118-
}
119-
}
120-
return nil
121-
}
122-
123142
debug("releases matching criteria: %s", releases)
143+
144+
backtracking_loop:
124145
for _, release := range releases {
125-
deps := release.GetDependencies()
126-
debug("try with %s %s", release, deps)
146+
releaseDeps := release.GetDependencies()
147+
debug("try with %s %s", release, releaseDeps)
127148

128-
if missingDep := findMissingDeps(deps); missingDep != nil {
129-
debug("%s did not work, becuase his dependency %s does not exists", release, missingDep.GetName())
130-
continue
149+
for _, releaseDep := range releaseDeps {
150+
if _, ok := ar.releases[releaseDep.GetName()]; !ok {
151+
debug("%s did not work, becuase his dependency %s does not exists", release, releaseDep.GetName())
152+
continue backtracking_loop
153+
}
131154
}
132155

133-
solution[depName] = release
134-
newDepsToProcess := append(depsToProcess[1:], deps...)
156+
ar.solution[depName] = release
157+
oldDepsToProcess := ar.depsToProcess
158+
ar.depsToProcess = append(ar.depsToProcess[1:], releaseDeps...)
135159
// bubble up problematics deps so they are processed first
136-
sort.Slice(newDepsToProcess, func(i, j int) bool {
137-
return problematicDeps[newDepsToProcess[i]] > problematicDeps[newDepsToProcess[j]]
160+
sort.Slice(ar.depsToProcess, func(i, j int) bool {
161+
ci := hashDependency(ar.depsToProcess[i])
162+
cj := hashDependency(ar.depsToProcess[j])
163+
return ar.problematicDeps[ci] > ar.problematicDeps[cj]
138164
})
139-
if res := ar.resolve(solution, newDepsToProcess, problematicDeps); res != nil {
165+
if res := ar.resolve(); res != nil {
140166
return res
141167
}
168+
ar.depsToProcess = oldDepsToProcess
142169
debug("%s did not work...", release)
143-
delete(solution, depName)
170+
delete(ar.solution, depName)
144171
}
145172

146-
problematicDeps[dep]++
173+
ar.problematicDeps[hashDependency(dep)]++
147174
return nil
148175
}

resolver_test.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ type customDep struct {
1919
cond Constraint
2020
}
2121

22+
// GetName return the name of the dependency (implements the Dependency interface)
2223
func (c *customDep) GetName() string {
2324
return c.name
2425
}
2526

27+
// GetConstraint return the version contraints of the dependency (implements the Dependency interface)
2628
func (c *customDep) GetConstraint() Constraint {
2729
return c.cond
2830
}
@@ -34,26 +36,28 @@ func (c *customDep) String() string {
3436
type customRel struct {
3537
name string
3638
vers *Version
37-
deps []Dependency
39+
deps []*customDep
3840
}
3941

42+
// GetName return the name of the release (implements the Release interface)
4043
func (r *customRel) GetName() string {
4144
return r.name
4245
}
4346

47+
// GetVersion return the version of the release (implements the Release interface)
4448
func (r *customRel) GetVersion() *Version {
4549
return r.vers
4650
}
4751

48-
func (r *customRel) GetDependencies() []Dependency {
52+
func (r *customRel) GetDependencies() []*customDep {
4953
return r.deps
5054
}
5155

5256
func (r *customRel) String() string {
5357
return r.name + "@" + r.vers.String()
5458
}
5559

56-
func d(dep string) Dependency {
60+
func d(dep string) *customDep {
5761
name := dep[0:1]
5862
cond, err := ParseConstraint(dep[1:])
5963
if err != nil {
@@ -62,15 +66,15 @@ func d(dep string) Dependency {
6266
return &customDep{name: name, cond: cond}
6367
}
6468

65-
func deps(deps ...string) []Dependency {
66-
res := []Dependency{}
69+
func deps(deps ...string) []*customDep {
70+
var res []*customDep
6771
for _, dep := range deps {
6872
res = append(res, d(dep))
6973
}
7074
return res
7175
}
7276

73-
func rel(name, ver string, deps []Dependency) Release {
77+
func rel(name, ver string, deps []*customDep) *customRel {
7478
return &customRel{name: name, vers: v(ver), deps: deps}
7579
}
7680

@@ -119,18 +123,18 @@ func TestResolver(t *testing.T) {
119123
i160 := rel("I", "1.6.0", deps())
120124
i170 := rel("I", "1.7.0", deps())
121125
i180 := rel("I", "1.8.0", deps())
122-
arch := &Archive{
123-
Releases: map[string]Releases{
124-
"A": {a100, a110, a111, a120, a121},
125-
"B": {b131, b130, b121, b120, b111, b110, b100},
126-
"C": {c200, c120, c111, c110, c102, c101, c100, c021, c020, c010},
127-
"D": {d100, d120},
128-
"E": {e100, e101},
129-
"G": {g130, g140, g150, g160, g170, g180},
130-
"H": {h130, h140, h150, h160, h170, h180},
131-
"I": {i130, i140, i150, i160, i170, i180},
132-
},
133-
}
126+
arch := NewResolver[*customRel, *customDep]()
127+
arch.AddReleases(
128+
a100, a110, a111, a120, a121,
129+
b131, b130, b121, b120, b111, b110, b100,
130+
c200, c120, c111, c110, c102, c101, c100, c021, c020, c010,
131+
d100, d120,
132+
g130, g140, g150, g160, g170, g180,
133+
h130, h140, h150, h160, h170, h180,
134+
i130, i140, i150, i160, i170, i180,
135+
)
136+
arch.AddRelease(e100) // use this method for 100% code coverage
137+
arch.AddRelease(e101)
134138

135139
a130 := rel("A", "1.3.0", deps())
136140
r0 := arch.Resolve(a130) // Non-existent in archive
@@ -179,4 +183,7 @@ func TestResolver(t *testing.T) {
179183
case <-time.After(time.Second):
180184
require.FailNow(t, "test didn't complete in the allocated time")
181185
}
186+
187+
r7 := arch.Resolve(e101)
188+
require.Nil(t, r7)
182189
}

0 commit comments

Comments
 (0)