solve 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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