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/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ script: "bundle exec thor spec"
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.2
5
+ - 1.9.3
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
1
  # Solve
2
+ [![Build Status](https://secure.travis-ci.org/reset/solve.png?branch=master)](http://travis-ci.org/reset/solve)
3
+ [![Dependency Status](https://gemnasium.com/reset/solve.png?travis)](https://gemnasium.com/reset/solve)
4
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/reset/solve)
2
5
 
3
6
  A Ruby constraint solver
4
7
 
@@ -20,20 +23,18 @@ Now add another artifact that has a dependency
20
23
 
21
24
  graph.artifacts("mysql", "1.2.4").depends("openssl", "~> 1.0.0")
22
25
 
23
- Setup some demands
26
+ Dependencies can be chained, too
24
27
 
25
- graph.demands('nginx', '>= 0.100.0')
28
+ graph.artifacts("ntp", "1.0.0").depends("build-essential").depends("yum")
26
29
 
27
- And now solve the graph
30
+ And now solve the graph with some demands
28
31
 
29
- Solve.it!(graph)
32
+ Solve.it!(graph, ['nginx', '>= 0.100.0'])
30
33
 
31
- ### Removing an artifact, demand, or dependency
34
+ ### Removing an artifact, or dependency from the graph
32
35
 
33
36
  graph.artifacts("nginx", "1.0.0").delete
34
37
 
35
- graph.demands('nginx', '>= 0.100.0').delete
36
-
37
38
  artifact.dependencies("nginx", "~> 1.0.0").delete
38
39
 
39
40
  ## Authors
data/lib/solve.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require 'solve/errors'
2
- require 'solve/core_ext'
3
- require 'dep_selector'
4
2
 
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
5
4
  module Solve
6
5
  autoload :Version, 'solve/version'
7
6
  autoload :Artifact, 'solve/artifact'
@@ -9,39 +8,21 @@ module Solve
9
8
  autoload :Dependency, 'solve/dependency'
10
9
  autoload :Graph, 'solve/graph'
11
10
  autoload :Demand, 'solve/demand'
11
+ autoload :Solver, 'solve/solver'
12
12
 
13
13
  class << self
14
- include DepSelector
15
-
16
- # @param [Solve::Graph] graph
14
+ # A quick solve. Given the "world" as we know it (the graph) and a list of
15
+ # requirements (demands) which must be met. Return me the best solution of
16
+ # artifacts and verisons that I should use.
17
17
  #
18
- # @return [Hash]
19
- def it(graph)
20
- it!(graph)
21
- rescue NoSolutionError
22
- nil
23
- end
24
-
25
18
  # @param [Solve::Graph] graph
19
+ # @param [Array<Solve::Demand>, Array<String, String>] demands
20
+ #
21
+ # @raise [NoSolutionError]
26
22
  #
27
23
  # @return [Hash]
28
- def it!(graph)
29
- dep_graph = graph.send(:dep_graph)
30
- selector = Selector.new(dep_graph)
31
-
32
- solution_constraints = graph.demands.collect do |demand|
33
- SolutionConstraint.new(dep_graph.package(demand.name), DepSelector::VersionConstraint.new(demand.constraint.to_s))
34
- end
35
-
36
- solution = quietly { selector.find_solution(solution_constraints) }
37
-
38
- {}.tap do |artifacts|
39
- solution.each do |name, constraint|
40
- artifacts[name] = constraint.to_s
41
- end
42
- end
43
- rescue DepSelector::Exceptions::InvalidSolutionConstraints
44
- raise NoSolutionError
24
+ def it!(graph, demands)
25
+ Solver.new(graph, demands).resolve
45
26
  end
46
27
  end
47
28
  end
@@ -1,7 +1,21 @@
1
1
  module Solve
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
2
3
  class Artifact
4
+ include Comparable
5
+
6
+ # A reference to the graph this artifact belongs to
7
+ #
8
+ # @return [Solve::Graph]
3
9
  attr_reader :graph
10
+
11
+ # The name of the artifact
12
+ #
13
+ # @return [String]
4
14
  attr_reader :name
15
+
16
+ # The version of this artifact
17
+ #
18
+ # @return [Solve::Version]
5
19
  attr_reader :version
6
20
 
7
21
  # @param [Solve::Graph] graph
@@ -14,73 +28,50 @@ module Solve
14
28
  @dependencies = Hash.new
15
29
  end
16
30
 
17
- # @overload dependencies(name, constraint)
18
- # Return the Solve::Dependency from the collection of
19
- # dependencies with the given name and constraint.
31
+ # Return the Solve::Dependency from the collection of
32
+ # dependencies with the given name and constraint.
20
33
  #
21
- # @param [#to_s]
22
- # @param [Solve::Constraint, #to_s]
34
+ # @param [#to_s] name
35
+ # @param [Solve::Constraint, #to_s] constraint
23
36
  #
24
- # @return [Solve::Dependency]
25
- # @overload dependencies
26
- # Return the collection of dependencies
37
+ # @example adding dependencies
38
+ # artifact.depends("nginx") => <#Dependency: @name="nginx", @constraint=">= 0.0.0">
39
+ # artifact.depends("ntp", "= 1.0.0") => <#Dependency: @name="ntp", @constraint="= 1.0.0">
27
40
  #
28
- # @return [Array<Solve::Dependency>]
29
- def dependencies(*args)
30
- if args.empty?
31
- return dependency_collection
32
- end
33
- if args.length > 2
34
- raise ArgumentError, "Unexpected number of arguments. You gave: #{args.length}. Expected: 2 or less."
35
- end
36
-
37
- name, constraint = args
38
- constraint ||= ">= 0.0.0"
39
-
41
+ # @example chaining dependencies
42
+ # artifact.depends("nginx").depends("ntp")
43
+ #
44
+ # @return [Solve::Artifact]
45
+ def depends(name, constraint = ">= 0.0.0")
40
46
  if name.nil?
41
47
  raise ArgumentError, "A name must be specified. You gave: #{args}."
42
48
  end
43
49
 
44
50
  dependency = Dependency.new(self, name, constraint)
45
51
  add_dependency(dependency)
46
- end
47
- alias_method :depends, :dependencies
48
52
 
49
- # Add a Solve::Dependency to the collection of dependencies
50
- # and return the added Solve::Dependency. No change will be
51
- # made if the dependency is already a member of the collection.
52
- #
53
- # @param [Solve::Dependency] dependency
54
- #
55
- # @return [Solve::Dependency]
56
- def add_dependency(dependency)
57
- unless has_dependency?(dependency)
58
- dep_graph = graph.send(:dep_graph)
59
- a = dep_graph.package(self.name).add_version(DepSelector::Version.new(self.version.to_s))
60
- dep_pack = dep_graph.package(dependency.name)
61
- a.dependencies << DepSelector::Dependency.new(dep_pack, DepSelector::VersionConstraint.new(dependency.constraint.to_s))
62
- @dependencies[dependency.to_s] = dependency
63
- end
64
-
65
- dependency
53
+ self
66
54
  end
67
55
 
68
- # @param [Solve::Dependency] dependency
56
+ # Return the collection of dependencies on this instance of artifact
69
57
  #
70
- # @return [Solve::Dependency, nil]
71
- def remove_dependency(dependency)
72
- if has_dependency?(dependency)
73
- @dependencies.delete(dependency.to_s)
74
- end
58
+ # @return [Array<Solve::Dependency>]
59
+ def dependencies
60
+ @dependencies.collect { |name, dependency| dependency }
75
61
  end
76
62
 
77
- # @param [Solve::Dependency] dependency
63
+ # Retrieve the dependency from the artifact with the matching name and constraint
78
64
  #
79
- # @return [Boolean]
80
- def has_dependency?(dependency)
81
- @dependencies.has_key?(dependency.to_s)
65
+ # @param [#to_s] name
66
+ # @param [#to_s] constraint
67
+ #
68
+ # @return [Solve::Artifact, nil]
69
+ def get_dependency(name, constraint)
70
+ @dependencies.fetch(Graph.dependency_key(name, constraint), nil)
82
71
  end
83
72
 
73
+ # Remove this artifact from the graph it belongs to
74
+ #
84
75
  # @return [Solve::Artifact, nil]
85
76
  def delete
86
77
  unless graph.nil?
@@ -94,11 +85,59 @@ module Solve
94
85
  "#{name}-#{version}"
95
86
  end
96
87
 
88
+ # @param [Object] other
89
+ #
90
+ # @return [Boolean]
91
+ def ==(other)
92
+ other.is_a?(self.class) &&
93
+ self.name == other.name &&
94
+ self.version == other.version
95
+ end
96
+ alias_method :eql?, :==
97
+
98
+ # @param [Solve::Version] other
99
+ #
100
+ # @return [Integer]
101
+ def <=>(other)
102
+ self.version <=> other.version
103
+ end
104
+
97
105
  private
98
106
 
99
- # @return [Array<Solve::Dependency>]
100
- def dependency_collection
101
- @dependencies.collect { |name, dependency| dependency }
107
+ # Add a Solve::Dependency to the collection of dependencies
108
+ # and return the added Solve::Dependency. No change will be
109
+ # made if the dependency is already a member of the collection.
110
+ #
111
+ # @param [Solve::Dependency] dependency
112
+ #
113
+ # @return [Solve::Dependency]
114
+ def add_dependency(dependency)
115
+ unless has_dependency?(dependency.name, dependency.constraint)
116
+ @dependencies[Graph.key_for(dependency)] = dependency
117
+ end
118
+
119
+ get_dependency(dependency.name, dependency.constraint)
120
+ end
121
+
122
+ # Remove the matching dependency from the artifact
123
+ #
124
+ # @param [Solve::Dependency] dependency
125
+ #
126
+ # @return [Solve::Dependency, nil]
127
+ def remove_dependency(dependency)
128
+ if has_dependency?(dependency)
129
+ @dependencies.delete(Graph.key_for(dependency))
130
+ end
131
+ end
132
+
133
+ # Check if the artifact has a dependency with the matching name and constraint
134
+ #
135
+ # @param [#to_s] name
136
+ # @param [#to_s] constraint
137
+ #
138
+ # @return [Boolean]
139
+ def has_dependency?(name, constraint)
140
+ @dependencies.has_key?(Graph.dependency_key(name, constraint))
102
141
  end
103
142
  end
104
143
  end
@@ -1,10 +1,29 @@
1
1
  module Solve
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
2
3
  class Constraint
3
4
  class << self
5
+ # Split a constraint string into an Array of two elements. The first
6
+ # element being the operator and second being the version string.
7
+ #
8
+ # If the given string does not contain a constraint operator then (=)
9
+ # will be used.
10
+ #
11
+ # If the given string does not contain a valid version string then
12
+ # nil will be returned.
13
+ #
4
14
  # @param [#to_s] string
5
15
  #
16
+ # @example splitting a string with a constraint operator and valid version string
17
+ # Constraint.split(">= 1.0.0") => [ ">=", "1.0.0" ]
18
+ #
19
+ # @example splitting a string without a constraint operator
20
+ # Constraint.split("0.0.0") => [ "=", "1.0.0" ]
21
+ #
22
+ # @example splitting a string without a valid version string
23
+ # Constraint.split("hello") => nil
24
+ #
6
25
  # @return [Array, nil]
7
- def parse(string)
26
+ def split(string)
8
27
  if string =~ /^[0-9]/
9
28
  op = "="
10
29
  ver = string
@@ -16,40 +35,108 @@ module Solve
16
35
 
17
36
  [ op, ver ]
18
37
  end
38
+
39
+ # @param [Solve::Constraint] constraint
40
+ # @param [Solve::Version] target_version
41
+ #
42
+ # @return [Boolean]
43
+ def compare_equal(constraint, target_version)
44
+ target_version == constraint.version
45
+ end
46
+
47
+ # @param [Solve::Constraint] constraint
48
+ # @param [Solve::Version] target_version
49
+ #
50
+ # @return [Boolean]
51
+ def compare_gt(constraint, target_version)
52
+ target_version > constraint.version
53
+ end
54
+
55
+ # @param [Solve::Constraint] constraint
56
+ # @param [Solve::Version] target_version
57
+ #
58
+ # @return [Boolean]
59
+ def compare_lt(constraint, target_version)
60
+ target_version < constraint.version
61
+ end
62
+
63
+ # @param [Solve::Constraint] constraint
64
+ # @param [Solve::Version] target_version
65
+ #
66
+ # @return [Boolean]
67
+ def compare_gte(constraint, target_version)
68
+ target_version >= constraint.version
69
+ end
70
+
71
+ # @param [Solve::Constraint] constraint
72
+ # @param [Solve::Version] target_version
73
+ #
74
+ # @return [Boolean]
75
+ def compare_lte(constraint, target_version)
76
+ target_version <= constraint.version
77
+ end
78
+
79
+ # @param [Solve::Constraint] constraint
80
+ # @param [Solve::Version] target_version
81
+ #
82
+ # @return [Boolean]
83
+ def compare_aprox(constraint, target_version)
84
+ unless constraint.patch.nil?
85
+ target_version.patch >= constraint.patch &&
86
+ target_version.minor == constraint.minor &&
87
+ target_version.major == constraint.major
88
+ else
89
+ target_version.minor >= constraint.minor &&
90
+ target_version.major == constraint.major
91
+ end
92
+ end
19
93
  end
20
94
 
21
- OPERATORS = [
22
- "=",
23
- ">",
24
- "<",
25
- ">=",
26
- "<=",
27
- "~>"
28
- ]
29
- REGEXP = /^(#{OPERATORS.join('|')}) (.+)$/
95
+ OPERATORS = {
96
+ "=" => method(:compare_equal),
97
+ ">" => method(:compare_gt),
98
+ "<" => method(:compare_lt),
99
+ ">=" => method(:compare_gte),
100
+ "<=" => method(:compare_lte),
101
+ "~>" => method(:compare_aprox)
102
+ }.freeze
103
+
104
+ REGEXP = /^(#{OPERATORS.keys.join('|')}) (.+)$/
30
105
 
31
106
  attr_reader :operator
32
- attr_reader :version
107
+ attr_reader :major
108
+ attr_reader :minor
109
+ attr_reader :patch
33
110
 
34
111
  # @param [#to_s] constraint
35
112
  def initialize(constraint = ">= 0.0.0")
36
- @operator, ver_str = self.class.parse(constraint)
113
+ @operator, ver_str = self.class.split(constraint)
37
114
  if @operator.nil? || ver_str.nil?
38
- raise InvalidConstraintFormat.new(constraint)
115
+ raise Errors::InvalidConstraintFormat.new(constraint)
39
116
  end
40
117
 
41
- @version = Version.new(ver_str)
42
- @dep_constraint = DepSelector::VersionConstraint.new(constraint)
118
+ @major, @minor, @patch = Version.split(ver_str)
119
+ @compare_fun = OPERATORS.fetch(self.operator)
120
+ end
121
+
122
+ # Return the Solve::Version representation of the major, minor, and patch
123
+ # attributes of this instance
124
+ #
125
+ # @return [Solve::Version]
126
+ def version
127
+ @version ||= Version.new([self.major, self.minor, self.patch])
43
128
  end
44
129
 
45
130
  # Returns true or false if the given version would be satisfied by
46
131
  # the version constraint.
47
132
  #
48
- # @param [Version, String] version
133
+ # @param [#to_s] target_version
49
134
  #
50
135
  # @return [Boolean]
51
- def satisfies?(version)
52
- dep_constraint.include?(version)
136
+ def satisfies?(target_version)
137
+ target_version = Version.new(target_version.to_s)
138
+
139
+ @compare_fun.call(self, target_version)
53
140
  end
54
141
 
55
142
  # @param [Object] other
@@ -58,16 +145,14 @@ module Solve
58
145
  def ==(other)
59
146
  other.is_a?(self.class) &&
60
147
  self.operator == other.operator &&
61
- self.version == other.version
148
+ self.major == other.minor &&
149
+ self.minor == other.minor &&
150
+ self.patch == other.patch
62
151
  end
63
152
  alias_method :eql?, :==
64
153
 
65
154
  def to_s
66
- "#{operator} #{version}"
155
+ "#{operator} #{major}.#{minor}.#{patch}"
67
156
  end
68
-
69
- private
70
-
71
- attr_reader :dep_constraint
72
157
  end
73
158
  end