solve 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/solve/demand.rb CHANGED
@@ -1,38 +1,47 @@
1
1
  module Solve
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
2
3
  class Demand
3
- attr_reader :graph
4
+ # A reference to the solver this demand belongs to
5
+ #
6
+ # @return [Solve::Solver]
7
+ attr_reader :solver
8
+
9
+ # The name of the artifact this demand is for
10
+ #
11
+ # @return [String]
4
12
  attr_reader :name
13
+
14
+ # The acceptable constraint of the artifact this demand is for
15
+ #
16
+ # @return [Solve::Constraint]
5
17
  attr_reader :constraint
6
18
 
7
- # @param [Solve::Graph] graph
19
+ # @param [Solve::Solver] solver
8
20
  # @param [#to_s] name
9
21
  # @param [Solve::Constraint, #to_s] constraint
10
- def initialize(graph, name, constraint = nil)
11
- @graph = graph
22
+ def initialize(solver, name, constraint = ">= 0.0.0")
23
+ @solver = solver
12
24
  @name = name
13
-
14
- if constraint
15
- @constraint = if constraint.is_a?(Solve::Constraint)
16
- constraint
17
- else
18
- Constraint.new(constraint.to_s)
19
- end
25
+ @constraint = if constraint.is_a?(Solve::Constraint)
26
+ constraint
27
+ else
28
+ Constraint.new(constraint.to_s)
20
29
  end
21
30
  end
22
31
 
32
+ # Remove this demand from the solver it belongs to
33
+ #
23
34
  # @return [Solve::Demand, nil]
24
35
  def delete
25
- unless graph.nil?
26
- result = graph.remove_demand(self)
27
- @graph = nil
36
+ unless solver.nil?
37
+ result = solver.remove_demand(self)
38
+ @solver = nil
28
39
  result
29
40
  end
30
41
  end
31
42
 
32
43
  def to_s
33
- s = "#{name}"
34
- s << "(#{constraint})" if constraint
35
- s
44
+ "#{name} (#{constraint})"
36
45
  end
37
46
  end
38
47
  end
@@ -1,13 +1,25 @@
1
1
  module Solve
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
2
3
  class Dependency
4
+ # A reference to the artifact this dependency belongs to
5
+ #
6
+ # @return [Solve::Artifact]
3
7
  attr_reader :artifact
8
+
9
+ # The name of the artifact this dependency represents
10
+ #
11
+ # @return [String]
4
12
  attr_reader :name
13
+
14
+ # The constraint requirement of this dependency
15
+ #
16
+ # @return [Solve::Constraint]
5
17
  attr_reader :constraint
6
18
 
7
19
  # @param [Solve::Artifact] artifact
8
20
  # @param [#to_s] name
9
21
  # @param [Solve::Constraint, #to_s] constraint
10
- def initialize(artifact, name, constraint)
22
+ def initialize(artifact, name, constraint = ">= 0.0.0")
11
23
  @artifact = artifact
12
24
  @name = name
13
25
  @constraint = case constraint
@@ -18,6 +30,8 @@ module Solve
18
30
  end
19
31
  end
20
32
 
33
+ # Remove this dependency from the artifact it belongs to
34
+ #
21
35
  # @return [Solve::Dependency, nil]
22
36
  def delete
23
37
  unless artifact.nil?
data/lib/solve/errors.rb CHANGED
@@ -1,31 +1,34 @@
1
1
  module Solve
2
- class SolveError < StandardError; end
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ module Errors
4
+ class SolveError < StandardError; end
3
5
 
4
- class InvalidVersionFormat < SolveError
5
- attr_reader :version
6
+ class InvalidVersionFormat < SolveError
7
+ attr_reader :version
6
8
 
7
- # @param [#to_s] version
8
- def initialize(version)
9
- @version = version
10
- end
9
+ # @param [#to_s] version
10
+ def initialize(version)
11
+ @version = version
12
+ end
11
13
 
12
- def message
13
- "'#{version}' did not contain a valid version string: 'x.y.z' or 'x.y'."
14
+ def message
15
+ "'#{version}' did not contain a valid version string: 'x.y.z' or 'x.y'."
16
+ end
14
17
  end
15
- end
16
18
 
17
- class InvalidConstraintFormat < SolveError
18
- attr_reader :constraint
19
+ class InvalidConstraintFormat < SolveError
20
+ attr_reader :constraint
19
21
 
20
- # @param [#to_s] constraint
21
- def initialize(constraint)
22
- @constraint = constraint
23
- end
22
+ # @param [#to_s] constraint
23
+ def initialize(constraint)
24
+ @constraint = constraint
25
+ end
24
26
 
25
- def message
26
- "'#{constraint}' did not contain a valid operator or a valid version string."
27
+ def message
28
+ "'#{constraint}' did not contain a valid operator or a valid version string."
29
+ end
27
30
  end
28
- end
29
31
 
30
- class NoSolutionError < SolveError; end
32
+ class NoSolutionError < SolveError; end
33
+ end
31
34
  end
@@ -1,3 +1,3 @@
1
1
  module Solve
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/solve/graph.rb CHANGED
@@ -1,9 +1,48 @@
1
1
  module Solve
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
2
3
  class Graph
4
+ class << self
5
+ # Create a key for a graph from an instance of an Artifact or Dependency
6
+ #
7
+ # @param [Solve::Artifact, Solve::Dependency] object
8
+ #
9
+ # @raise [ArgumentError] if an instance of an object of an unknown type is given
10
+ #
11
+ # @return [Symbol]
12
+ def key_for(object)
13
+ case object
14
+ when Solve::Artifact
15
+ artifact_key(object.name, object.version)
16
+ when Solve::Dependency
17
+ dependency_key(object.name, object.constraint)
18
+ else
19
+ raise ArgumentError, "Could not generate graph key for Class: #{object.class}"
20
+ end
21
+ end
22
+
23
+ # Create a key representing an artifact for an instance of Graph
24
+ #
25
+ # @param [#to_s] name
26
+ # @param [#to_s] version
27
+ #
28
+ # @return [Symbol]
29
+ def artifact_key(name, version)
30
+ "#{name}-#{version}".to_sym
31
+ end
32
+
33
+ # Create a key representing an dependency for an instance of Graph
34
+ #
35
+ # @param [#to_s] name
36
+ # @param [#to_s] constraint
37
+ #
38
+ # @return [Symbol]
39
+ def dependency_key(name, constraint)
40
+ "#{name}-#{constraint}".to_sym
41
+ end
42
+ end
43
+
3
44
  def initialize
4
45
  @artifacts = Hash.new
5
- @demands = Hash.new
6
- @dep_graph = DepSelector::DependencyGraph.new
7
46
  end
8
47
 
9
48
  # @overload artifacts(name, version)
@@ -36,6 +75,20 @@ module Solve
36
75
  add_artifact(artifact)
37
76
  end
38
77
 
78
+ # Return all the artifacts from the collection of artifacts
79
+ # with the given name.
80
+ #
81
+ # @param [String] name
82
+ #
83
+ # @return [Array<Solve::Artifact>]
84
+ def versions(name, constraint = ">= 0.0.0")
85
+ constraint = constraint.is_a?(Constraint) ? constraint : Constraint.new(constraint)
86
+
87
+ artifacts.select do |art|
88
+ art.name == name && constraint.satisfies?(art.version)
89
+ end
90
+ end
91
+
39
92
  # Add a Solve::Artifact to the collection of artifacts and
40
93
  # return the added Solve::Artifact. No change will be made
41
94
  # if the artifact is already a member of the collection.
@@ -44,109 +97,48 @@ module Solve
44
97
  #
45
98
  # @return [Solve::Artifact]
46
99
  def add_artifact(artifact)
47
- unless has_artifact?(artifact)
48
- @dep_graph.package(artifact.name).add_version(DepSelector::Version.new(artifact.version.to_s))
49
- @artifacts[artifact.to_s] = artifact
100
+ unless has_artifact?(artifact.name, artifact.version)
101
+ @artifacts[self.class.key_for(artifact)] = artifact
50
102
  end
51
103
 
52
- artifact
53
- end
54
-
55
- # @param [Solve::Artifact, nil] artifact
56
- def remove_artifact(artifact)
57
- if has_artifact?(artifact)
58
- @dep_graph.packages.delete(artifact.to_s)
59
- @artifacts.delete(artifact.to_s)
60
- end
61
- end
62
-
63
- # @param [Solve::Artifact] artifact
64
- #
65
- # @return [Boolean]
66
- def has_artifact?(artifact)
67
- @artifacts.has_key?(artifact.to_s)
104
+ get_artifact(artifact.name, artifact.version)
68
105
  end
69
106
 
70
- # @overload demands(name, constraint)
71
- # Return the Solve::Demand from the collection of demands
72
- # with the given name and constraint.
107
+ # Retrieve the artifact from the graph with the matching name and version
73
108
  #
74
- # @param [#to_s]
75
- # @param [Solve::Constraint, #to_s]
76
- #
77
- # @return [Solve::Demand]
78
- # @overload demands(name)
79
- # Return the Solve::Demand from the collection of demands
80
- # with the given name.
81
- #
82
- # @param [#to_s]
83
- #
84
- # @return [Solve::Demand]
85
- # @overload demands
86
- # Return the collection of demands
109
+ # @param [String] name
110
+ # @param [Solve::Version, #to_s] version
87
111
  #
88
- # @return [Array<Solve::Demand>]
89
- def demands(*args)
90
- if args.empty?
91
- return demand_collection
92
- end
93
- if args.length > 2
94
- raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
95
- end
96
-
97
- name, constraint = args
98
- constraint ||= ">= 0.0.0"
99
-
100
- if name.nil?
101
- raise ArgumentError, "A name must be specified. You gave: #{args}."
102
- end
103
-
104
- demand = Demand.new(self, name, constraint)
105
- add_demand(demand)
112
+ # @return [Solve::Artifact, nil]
113
+ def get_artifact(name, version)
114
+ @artifacts.fetch(self.class.artifact_key(name, version.to_s), nil)
106
115
  end
107
116
 
108
- # Add a Solve::Demand to the collection of demands and
109
- # return the added Solve::Demand. No change will be made
110
- # if the demand is already a member of the collection.
117
+ # Remove the given instance of artifact from the graph
111
118
  #
112
- # @param [Solve::Demand] demand
113
- #
114
- # @return [Solve::Demand]
115
- def add_demand(demand)
116
- unless has_demand?(demand)
117
- @demands[demand.to_s] = demand
118
- end
119
-
120
- demand
121
- end
122
- alias_method :demand, :add_demand
123
-
124
- # @param [Solve::Demand, nil] demand
125
- def remove_demand(demand)
126
- if has_demand?(demand)
127
- @demands.delete(demand.to_s)
119
+ # @param [Solve::Artifact, nil] artifact
120
+ def remove_artifact(artifact)
121
+ if has_artifact?(artifact.name, artifact.version)
122
+ @artifacts.delete(self.class.key_for(artifact))
128
123
  end
129
124
  end
130
125
 
131
- # @param [Solve::Demand] demand
126
+ # Check if an artifact with a matching name and version is a member of this instance
127
+ # of graph
128
+ #
129
+ # @param [String] name
130
+ # @param [Solve::Version, #to_s] version
132
131
  #
133
132
  # @return [Boolean]
134
- def has_demand?(demand)
135
- @demands.has_key?(demand.to_s)
133
+ def has_artifact?(name, version)
134
+ !get_artifact(name, version).nil?
136
135
  end
137
136
 
138
137
  private
139
138
 
140
- attr_reader :dep_graph
141
-
142
139
  # @return [Array<Solve::Artifact>]
143
140
  def artifact_collection
144
141
  @artifacts.collect { |name, artifact| artifact }
145
142
  end
146
-
147
- # @return [Array<Solve::Demand>]
148
- def demand_collection
149
- @demands.collect { |name, demand| demand }
150
- end
151
143
  end
152
144
  end
@@ -0,0 +1,246 @@
1
+ module Solve
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class Solver
4
+ autoload :VariableTable, 'solve/solver/variable_table'
5
+ autoload :Variable, 'solve/solver/variable'
6
+ autoload :ConstraintTable, 'solve/solver/constraint_table'
7
+ autoload :ConstraintRow, 'solve/solver/constraint_row'
8
+
9
+ class << self
10
+ # Create a key to identify a demand on a Solver.
11
+ #
12
+ # @param [Solve::Demand] demand
13
+ #
14
+ # @raise [NoSolutionError]
15
+ #
16
+ # @return [Symbol]
17
+ def demand_key(demand)
18
+ "#{demand.name}-#{demand.constraint}".to_sym
19
+ end
20
+
21
+ # Returns all of the versions which satisfy all of the given constraints
22
+ #
23
+ # @param [Array<Solve::Constraint>, Array<String>] constraints
24
+ # @param [Array<Solve::Version>, Array<String>] versions
25
+ #
26
+ # @return [Array<Solve::Version>]
27
+ def satisfy_all(constraints, versions)
28
+ constraints = Array(constraints).collect do |con|
29
+ con.is_a?(Constraint) ? con : Constraint.new(con.to_s)
30
+ end.uniq
31
+
32
+ versions = Array(versions).collect do |ver|
33
+ ver.is_a?(Version) ? ver : Version.new(ver.to_s)
34
+ end.uniq
35
+
36
+ versions.select do |ver|
37
+ constraints.all? { |constraint| constraint.satisfies?(ver) }
38
+ end
39
+ end
40
+
41
+ # Return the best version from the given list of versions for the given list of constraints
42
+ #
43
+ # @param [Array<Solve::Constraint>, Array<String>] constraints
44
+ # @param [Array<Solve::Version>, Array<String>] versions
45
+ #
46
+ # @raise [NoSolutionError] if version matches the given constraints
47
+ #
48
+ # @return [Solve::Version]
49
+ def satisfy_best(constraints, versions)
50
+ solution = satisfy_all(constraints, versions)
51
+
52
+ if solution.empty?
53
+ raise Errors::NoSolutionError
54
+ end
55
+
56
+ solution.sort.last
57
+ end
58
+ end
59
+
60
+ # The world as we know it
61
+ #
62
+ # @return [Solve::Graph]
63
+ attr_reader :graph
64
+
65
+ attr_reader :domain
66
+ attr_reader :variable_table
67
+ attr_reader :constraint_table
68
+ attr_reader :possible_values
69
+
70
+ # @param [Solve::Graph] graph
71
+ # @param [Array<String>, Array<Array<String, String>>] demands
72
+ def initialize(graph, demands = Array.new)
73
+ @graph = graph
74
+ @domain = Hash.new
75
+ @demands = Hash.new
76
+ @possible_values = Hash.new
77
+ @constraint_table = ConstraintTable.new
78
+ @variable_table = VariableTable.new
79
+
80
+ Array(demands).each do |l_demand|
81
+ demands(*l_demand)
82
+ end
83
+ end
84
+
85
+ # @return [Hash]
86
+ def resolve
87
+ seed_demand_dependencies
88
+
89
+ while unbound_variable = variable_table.first_unbound
90
+ possible_values_for_unbound = possible_values_for(unbound_variable)
91
+
92
+ while possible_value = possible_values_for_unbound.shift
93
+ possible_artifact = graph.get_artifact(unbound_variable.package, possible_value.version)
94
+ possible_dependencies = possible_artifact.dependencies
95
+ all_ok = possible_dependencies.all? { |dependency| can_add_new_constraint?(dependency) }
96
+ if all_ok
97
+ add_dependencies(possible_dependencies, possible_artifact)
98
+ unbound_variable.bind(possible_value)
99
+ break
100
+ end
101
+ end
102
+
103
+ unless unbound_variable.bound?
104
+ backtrack(unbound_variable)
105
+ end
106
+ end
107
+
108
+ {}.tap do |solution|
109
+ variable_table.rows.each do |variable|
110
+ solution[variable.package] = variable.value.version.to_s
111
+ end
112
+ end
113
+ end
114
+
115
+ # @overload demands(name, constraint)
116
+ # Return the Solve::Demand from the collection of demands
117
+ # with the given name and constraint.
118
+ #
119
+ # @param [#to_s]
120
+ # @param [Solve::Constraint, #to_s]
121
+ #
122
+ # @return [Solve::Demand]
123
+ # @overload demands(name)
124
+ # Return the Solve::Demand from the collection of demands
125
+ # with the given name.
126
+ #
127
+ # @param [#to_s]
128
+ #
129
+ # @return [Solve::Demand]
130
+ # @overload demands
131
+ # Return the collection of demands
132
+ #
133
+ # @return [Array<Solve::Demand>]
134
+ def demands(*args)
135
+ if args.empty?
136
+ return demand_collection
137
+ end
138
+ if args.length > 2
139
+ raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
140
+ end
141
+
142
+ name, constraint = args
143
+ constraint ||= ">= 0.0.0"
144
+
145
+ if name.nil?
146
+ raise ArgumentError, "A name must be specified. You gave: #{args}."
147
+ end
148
+
149
+ demand = Demand.new(self, name, constraint)
150
+ add_demand(demand)
151
+ end
152
+
153
+ # Add a Solve::Demand to the collection of demands and
154
+ # return the added Solve::Demand. No change will be made
155
+ # if the demand is already a member of the collection.
156
+ #
157
+ # @param [Solve::Demand] demand
158
+ #
159
+ # @return [Solve::Demand]
160
+ def add_demand(demand)
161
+ unless has_demand?(demand)
162
+ @demands[self.class.demand_key(demand)] = demand
163
+ end
164
+
165
+ demand
166
+ end
167
+ alias_method :demand, :add_demand
168
+
169
+ # @param [Solve::Demand, nil] demand
170
+ def remove_demand(demand)
171
+ if has_demand?(demand)
172
+ @demands.delete(self.class.demand_key(demand))
173
+ end
174
+ end
175
+
176
+ # @param [Solve::Demand] demand
177
+ #
178
+ # @return [Boolean]
179
+ def has_demand?(demand)
180
+ @demands.has_key?(self.class.demand_key(demand))
181
+ end
182
+
183
+ private
184
+
185
+ # @return [Array<Solve::Demand>]
186
+ def demand_collection
187
+ @demands.collect { |name, demand| demand }
188
+ end
189
+
190
+ def seed_demand_dependencies
191
+ add_dependencies(demands, :root)
192
+ end
193
+
194
+ def can_add_new_constraint?(dependency)
195
+ current_binding = variable_table.find_package(dependency.name)
196
+ #haven't seen it before, haven't bound it yet or the binding is ok
197
+ current_binding.nil? || current_binding.value.nil? || dependency.constraint.satisfies?(current_binding.value.version)
198
+ end
199
+
200
+ def possible_values_for(variable)
201
+ possible_values_for_variable = possible_values[variable.package]
202
+ if possible_values_for_variable.nil?
203
+ constraints_for_variable = constraint_table.constraints_on_package(variable.package)
204
+ all_values_for_variable = domain[variable.package]
205
+ possible_values_for_variable = constraints_for_variable.inject(all_values_for_variable) do |remaining_values, constraint|
206
+ remaining_values.reject { |value| !constraint.satisfies?(value.version) }
207
+ end
208
+ possible_values[variable.package] = possible_values_for_variable
209
+ end
210
+ possible_values_for_variable
211
+ end
212
+
213
+ def add_dependencies(dependencies, source)
214
+ dependencies.each do |dependency|
215
+ variable_table.add(dependency.name, source)
216
+ constraint_table.add(dependency.name, dependency.constraint, source)
217
+ dependency_domain = graph.versions(dependency.name, dependency.constraint)
218
+ domain[dependency.name] = [(domain[dependency.name] || []), dependency_domain]
219
+ .flatten
220
+ .uniq
221
+ .sort { |left, right| right.version <=> left.version }
222
+ end
223
+ end
224
+
225
+ def reset_possible_values_for(variable)
226
+ possible_values[variable.package] = nil
227
+ possible_values_for(variable)
228
+ end
229
+
230
+ def backtrack(unbound_variable)
231
+ previous_variable = variable_table.before(unbound_variable.package)
232
+
233
+ if previous_variable.nil?
234
+ raise Errors::NoSolutionError
235
+ end
236
+
237
+ source = previous_variable.value
238
+ variable_table.remove_all_with_only_this_source!(source)
239
+ constraint_table.remove_constraints_from_source!(source)
240
+ previous_variable.unbind
241
+ variable_table.all_after(previous_variable.package).each do |variable|
242
+ new_possibles = reset_possible_values_for(variable)
243
+ end
244
+ end
245
+ end
246
+ end