semantic_puppet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ require 'semantic_puppet/dependency'
2
+ require 'set'
3
+
4
+ module SemanticPuppet
5
+ module Dependency
6
+ module GraphNode
7
+ include Comparable
8
+
9
+ def name
10
+ end
11
+
12
+ # Determines whether the modules dependencies are satisfied by the known
13
+ # releases.
14
+ #
15
+ # @return [Boolean] true if all dependencies are satisfied
16
+ def satisfied?
17
+ dependencies.none? { |_, v| v.empty? }
18
+ end
19
+
20
+ def children
21
+ @_children ||= {}
22
+ end
23
+
24
+ def populate_children(nodes)
25
+ if children.empty?
26
+ nodes = nodes.select { |node| satisfies_dependency?(node) }
27
+ nodes.each do |node|
28
+ children[node.name] = node
29
+ node.populate_children(nodes)
30
+ end
31
+ self.freeze
32
+ end
33
+ end
34
+
35
+ # @api internal
36
+ # @return [{ String => SortedSet<GraphNode> }] the satisfactory
37
+ # dependency nodes
38
+ def dependencies
39
+ @_dependencies ||= Hash.new { |h, k| h[k] = SortedSet.new }
40
+ end
41
+
42
+ # Adds the given dependency name to the list of dependencies.
43
+ #
44
+ # @param name [String] the dependency name
45
+ # @return [void]
46
+ def add_dependency(name)
47
+ dependencies[name]
48
+ end
49
+
50
+ # @return [Array<String>] the list of dependency names
51
+ def dependency_names
52
+ dependencies.keys
53
+ end
54
+
55
+ def constraints
56
+ @_constraints ||= Hash.new { |h, k| h[k] = [] }
57
+ end
58
+
59
+ def constraints_for(name)
60
+ return [] unless constraints.has_key?(name)
61
+
62
+ constraints[name].map do |constraint|
63
+ {
64
+ :source => constraint[0],
65
+ :description => constraint[1],
66
+ :test => constraint[2],
67
+ }
68
+ end
69
+ end
70
+
71
+ # Constrains the named module to suitable releases, as determined by the
72
+ # given block.
73
+ #
74
+ # @example Version-locking currently installed modules
75
+ # installed_modules.each do |m|
76
+ # @graph.add_constraint('installed', m.name, m.version) do |node|
77
+ # m.version == node.version
78
+ # end
79
+ # end
80
+ #
81
+ # @param source [String, Symbol] a name describing the source of the
82
+ # constraint
83
+ # @param mod [String] the name of the module
84
+ # @param desc [String] a description of the enforced constraint
85
+ # @yieldparam node [GraphNode] the node to test the constraint against
86
+ # @yieldreturn [Boolean] whether the node passed the constraint
87
+ # @return [void]
88
+ def add_constraint(source, mod, desc, &block)
89
+ constraints["#{mod}"] << [ source, desc, block ]
90
+ end
91
+
92
+ def satisfies_dependency?(node)
93
+ dependencies.key?(node.name) && satisfies_constraints?(node)
94
+ end
95
+
96
+ # @param release [ModuleRelease] the release to test
97
+ def satisfies_constraints?(release)
98
+ constraints_for(release.name).all? { |x| x[:test].call(release) }
99
+ end
100
+
101
+ def << (nodes)
102
+ Array(nodes).each do |node|
103
+ next unless dependencies.key?(node.name)
104
+ if satisfies_dependency?(node)
105
+ dependencies[node.name] << node
106
+ end
107
+ end
108
+
109
+ return self
110
+ end
111
+
112
+ def <=>(other)
113
+ name <=> other.name
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,46 @@
1
+ require 'semantic_puppet/dependency'
2
+
3
+ module SemanticPuppet
4
+ module Dependency
5
+ class ModuleRelease
6
+ include GraphNode
7
+
8
+ attr_reader :name, :version
9
+
10
+ # Create a new instance of a module release.
11
+ #
12
+ # @param source [SemanticPuppet::Dependency::Source]
13
+ # @param name [String]
14
+ # @param version [SemanticPuppet::Version]
15
+ # @param dependencies [{String => SemanticPuppet::VersionRange}]
16
+ def initialize(source, name, version, dependencies = {})
17
+ @source = source
18
+ @name = name.freeze
19
+ @version = version.freeze
20
+
21
+ dependencies.each do |name, range|
22
+ add_constraint('initialize', name, range.to_s) do |node|
23
+ range === node.version
24
+ end
25
+
26
+ add_dependency(name)
27
+ end
28
+ end
29
+
30
+ def priority
31
+ @source.priority
32
+ end
33
+
34
+ def <=>(oth)
35
+ our_key = [ priority, name, version ]
36
+ their_key = [ oth.priority, oth.name, oth.version ]
37
+
38
+ return our_key <=> their_key
39
+ end
40
+
41
+ def to_s
42
+ "#<#{self.class} #{name}@#{version}>"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ require 'semantic_puppet/dependency'
2
+
3
+ module SemanticPuppet
4
+ module Dependency
5
+ class Source
6
+ def self.priority
7
+ 0
8
+ end
9
+
10
+ def priority
11
+ self.class.priority
12
+ end
13
+
14
+ def create_release(name, version, dependencies = {})
15
+ version = Version.parse(version) if version.is_a? String
16
+ dependencies = dependencies.inject({}) do |hash, (key, value)|
17
+ hash[key] = VersionRange.parse(value || '>= 0.0.0')
18
+ hash[key] ||= VersionRange::EMPTY_RANGE
19
+ hash
20
+ end
21
+ ModuleRelease.new(self, name, version, dependencies)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require 'semantic_puppet/dependency'
2
+
3
+ module SemanticPuppet
4
+ module Dependency
5
+ class UnsatisfiableGraph < StandardError
6
+ attr_reader :graph
7
+
8
+ def initialize(graph)
9
+ @graph = graph
10
+
11
+ deps = sentence_from_list(graph.modules)
12
+ super "Could not find satisfying releases for #{deps}"
13
+ end
14
+
15
+ private
16
+
17
+ def sentence_from_list(list)
18
+ case list.length
19
+ when 1
20
+ list.first
21
+ when 2
22
+ list.join(' and ')
23
+ else
24
+ list = list.dup
25
+ list.push("and #{list.pop}")
26
+ list.join(', ')
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,185 @@
1
+ require 'semantic_puppet'
2
+
3
+ module SemanticPuppet
4
+
5
+ # @note SemanticPuppet::Version subclasses Numeric so that it has sane Range
6
+ # semantics in Ruby 1.9+.
7
+ class Version < Numeric
8
+ include Comparable
9
+
10
+ class ValidationFailure < ArgumentError; end
11
+
12
+ class << self
13
+ # Parse a Semantic Version string.
14
+ #
15
+ # @param ver [String] the version string to parse
16
+ # @return [Version] a comparable {Version} object
17
+ def parse(ver)
18
+ match, major, minor, patch, prerelease, build = *ver.match(/\A#{REGEX_FULL}\Z/)
19
+
20
+ if match.nil?
21
+ raise "Unable to parse '#{ver}' as a semantic version identifier"
22
+ end
23
+
24
+ prerelease = parse_prerelease(prerelease) if prerelease
25
+ # Build metadata is not yet supported in semantic_puppet, but we hope to.
26
+ # The following code prevents build metadata for now.
27
+ #build = parse_build_metadata(build) if build
28
+ if !build.nil?
29
+ raise "'#{ver}' MUST NOT include build identifiers"
30
+ end
31
+
32
+ self.new(major.to_i, minor.to_i, patch.to_i, prerelease, build)
33
+ end
34
+
35
+ # Validate a Semantic Version string.
36
+ #
37
+ # @param ver [String] the version string to validate
38
+ # @return [bool] whether or not the string represents a valid Semantic Version
39
+ def valid?(ver)
40
+ !!(ver =~ /\A#{REGEX_FULL}\Z/)
41
+ end
42
+
43
+ private
44
+ def parse_prerelease(prerelease)
45
+ subject = 'Prerelease identifiers'
46
+ prerelease = prerelease.split('.', -1)
47
+
48
+ if prerelease.empty? or prerelease.any? { |x| x.empty? }
49
+ raise "#{subject} MUST NOT be empty"
50
+ elsif prerelease.any? { |x| x =~ /[^0-9a-zA-Z-]/ }
51
+ raise "#{subject} MUST use only ASCII alphanumerics and hyphens"
52
+ elsif prerelease.any? { |x| x =~ /^0\d+$/ }
53
+ raise "#{subject} MUST NOT contain leading zeroes"
54
+ end
55
+
56
+ return prerelease.map { |x| x =~ /^\d+$/ ? x.to_i : x }
57
+ end
58
+
59
+ def parse_build_metadata(build)
60
+ subject = 'Build identifiers'
61
+ build = build.split('.', -1)
62
+
63
+ if build.empty? or build.any? { |x| x.empty? }
64
+ raise "#{subject} MUST NOT be empty"
65
+ elsif build.any? { |x| x =~ /[^0-9a-zA-Z-]/ }
66
+ raise "#{subject} MUST use only ASCII alphanumerics and hyphens"
67
+ end
68
+
69
+ return build
70
+ end
71
+
72
+ def raise(msg)
73
+ super ValidationFailure, msg, caller.drop_while { |x| x !~ /\bparse\b/ }
74
+ end
75
+ end
76
+
77
+ attr_reader :major, :minor, :patch
78
+
79
+ def initialize(major, minor, patch, prerelease = nil, build = nil)
80
+ @major = major
81
+ @minor = minor
82
+ @patch = patch
83
+ @prerelease = prerelease
84
+ @build = build
85
+ end
86
+
87
+ def next(part)
88
+ case part
89
+ when :major
90
+ self.class.new(@major.next, 0, 0)
91
+ when :minor
92
+ self.class.new(@major, @minor.next, 0)
93
+ when :patch
94
+ self.class.new(@major, @minor, @patch.next)
95
+ end
96
+ end
97
+
98
+ def prerelease
99
+ @prerelease && @prerelease.join('.')
100
+ end
101
+
102
+ # @return [Boolean] true if this is a stable release
103
+ def stable?
104
+ @prerelease.nil?
105
+ end
106
+
107
+ def build
108
+ @build && @build.join('.')
109
+ end
110
+
111
+ def <=>(other)
112
+ return self.major <=> other.major unless self.major == other.major
113
+ return self.minor <=> other.minor unless self.minor == other.minor
114
+ return self.patch <=> other.patch unless self.patch == other.patch
115
+ return compare_prerelease(other)
116
+ end
117
+
118
+ def to_s
119
+ "#{major}.#{minor}.#{patch}" +
120
+ (@prerelease.nil? || prerelease.empty? ? '' : "-" + prerelease) +
121
+ (@build.nil? || build.empty? ? '' : "+" + build )
122
+ end
123
+
124
+ def hash
125
+ self.to_s.hash
126
+ end
127
+
128
+ private
129
+ # This is a hack; tildes sort later than any valid identifier. The
130
+ # advantage is that we don't need to handle stable vs. prerelease
131
+ # comparisons separately.
132
+ @@STABLE_RELEASE = [ '~' ].freeze
133
+
134
+ def compare_prerelease(other)
135
+ all_mine = @prerelease || @@STABLE_RELEASE
136
+ all_yours = other.instance_variable_get(:@prerelease) || @@STABLE_RELEASE
137
+
138
+ # Precedence is determined by comparing each dot separated identifier from
139
+ # left to right...
140
+ size = [ all_mine.size, all_yours.size ].max
141
+ Array.new(size).zip(all_mine, all_yours) do |_, mine, yours|
142
+
143
+ # ...until a difference is found.
144
+ next if mine == yours
145
+
146
+ # Numbers are compared numerically, strings are compared ASCIIbetically.
147
+ if mine.class == yours.class
148
+ return mine <=> yours
149
+
150
+ # A larger set of pre-release fields has a higher precedence.
151
+ elsif mine.nil?
152
+ return -1
153
+ elsif yours.nil?
154
+ return 1
155
+
156
+ # Numeric identifiers always have lower precedence than non-numeric.
157
+ elsif mine.is_a? Numeric
158
+ return -1
159
+ elsif yours.is_a? Numeric
160
+ return 1
161
+ end
162
+ end
163
+
164
+ return 0
165
+ end
166
+
167
+ def first_prerelease
168
+ self.class.new(@major, @minor, @patch, [])
169
+ end
170
+
171
+ public
172
+
173
+ # Version string matching regexes
174
+ REGEX_NUMERIC = "(0|[1-9]\\d*)[.](0|[1-9]\\d*)[.](0|[1-9]\\d*)" # Major . Minor . Patch
175
+ REGEX_PRE = "(?:[-](.*?))?" # Prerelease
176
+ REGEX_BUILD = "(?:[+](.*?))?" # Build
177
+ REGEX_FULL = REGEX_NUMERIC + REGEX_PRE + REGEX_BUILD
178
+
179
+ # The lowest precedence Version possible
180
+ MIN = self.new(0, 0, 0, []).freeze
181
+
182
+ # The highest precedence Version possible
183
+ MAX = self.new((1.0/0.0), 0, 0).freeze
184
+ end
185
+ end
@@ -0,0 +1,422 @@
1
+ require 'semantic_puppet'
2
+
3
+ module SemanticPuppet
4
+ class VersionRange < Range
5
+ class << self
6
+ # Parses a version range string into a comparable {VersionRange} instance.
7
+ #
8
+ # Currently parsed version range string may take any of the following:
9
+ # forms:
10
+ #
11
+ # * Regular Semantic Version strings
12
+ # * ex. `"1.0.0"`, `"1.2.3-pre"`
13
+ # * Partial Semantic Version strings
14
+ # * ex. `"1.0.x"`, `"1"`, `"2.X"`
15
+ # * Inequalities
16
+ # * ex. `"> 1.0.0"`, `"<3.2.0"`, `">=4.0.0"`
17
+ # * Approximate Versions
18
+ # * ex. `"~1.0.0"`, `"~ 3.2.0"`, `"~4.0.0"`
19
+ # * Inclusive Ranges
20
+ # * ex. `"1.0.0 - 1.3.9"`
21
+ # * Range Intersections
22
+ # * ex. `">1.0.0 <=2.3.0"`
23
+ #
24
+ # @param range_str [String] the version range string to parse
25
+ # @return [VersionRange] a new {VersionRange} instance
26
+ def parse(range_str)
27
+ partial = '\d+(?:[.]\d+)?(?:[.][x]|[.]\d+(?:[-][0-9a-z.-]*)?)?'
28
+ exact = '\d+[.]\d+[.]\d+(?:[-][0-9a-z.-]*)?'
29
+
30
+ range = range_str.gsub(/([(><=~])[ ]+/, '\1')
31
+ range = range.gsub(/ - /, '#').strip
32
+
33
+ return case range
34
+ when /\A(#{partial})\Z/i
35
+ parse_loose_version_expression($1)
36
+ when /\A([><][=]?)(#{exact})\Z/i
37
+ parse_inequality_expression($1, $2)
38
+ when /\A~(#{partial})\Z/i
39
+ parse_reasonably_close_expression($1)
40
+ when /\A(#{exact})#(#{exact})\Z/i
41
+ parse_inclusive_range_expression($1, $2)
42
+ when /[ ]+/
43
+ parse_intersection_expression(range)
44
+ else
45
+ raise ArgumentError
46
+ end
47
+
48
+ rescue ArgumentError
49
+ raise ArgumentError, "Unparsable version range: #{range_str.inspect}"
50
+ end
51
+
52
+ private
53
+
54
+ # Creates a new {VersionRange} from a range intersection expression.
55
+ #
56
+ # @param expr [String] a range intersection expression
57
+ # @return [VersionRange] a version range representing `expr`
58
+ def parse_intersection_expression(expr)
59
+ expr.split(/[ ]+/).map { |x| parse(x) }.inject { |a,b| a & b }
60
+ end
61
+
62
+ # Creates a new {VersionRange} from a "loose" description of a Semantic
63
+ # Version number.
64
+ #
65
+ # @see .process_loose_expr
66
+ #
67
+ # @param expr [String] a "loose" version expression
68
+ # @return [VersionRange] a version range representing `expr`
69
+ def parse_loose_version_expression(expr)
70
+ start, finish = process_loose_expr(expr)
71
+
72
+ if start.stable?
73
+ start = start.send(:first_prerelease)
74
+ end
75
+
76
+ if finish.stable?
77
+ exclude = true
78
+ finish = finish.send(:first_prerelease)
79
+ end
80
+
81
+ self.new(start, finish, exclude)
82
+ end
83
+
84
+ # Creates an open-ended version range from an inequality expression.
85
+ #
86
+ # @overload parse_inequality_expression('<', expr)
87
+ # {include:.parse_lt_expression}
88
+ #
89
+ # @overload parse_inequality_expression('<=', expr)
90
+ # {include:.parse_lte_expression}
91
+ #
92
+ # @overload parse_inequality_expression('>', expr)
93
+ # {include:.parse_gt_expression}
94
+ #
95
+ # @overload parse_inequality_expression('>=', expr)
96
+ # {include:.parse_gte_expression}
97
+ #
98
+ # @param comp ['<', '<=', '>', '>='] an inequality operator
99
+ # @param expr [String] a "loose" version expression
100
+ # @return [VersionRange] a range covering all versions in the inequality
101
+ def parse_inequality_expression(comp, expr)
102
+ case comp
103
+ when '>'
104
+ parse_gt_expression(expr)
105
+ when '>='
106
+ parse_gte_expression(expr)
107
+ when '<'
108
+ parse_lt_expression(expr)
109
+ when '<='
110
+ parse_lte_expression(expr)
111
+ end
112
+ end
113
+
114
+ # Returns a range covering all versions greater than the given `expr`.
115
+ #
116
+ # @param expr [String] the version to be greater than
117
+ # @return [VersionRange] a range covering all versions greater than the
118
+ # given `expr`
119
+ def parse_gt_expression(expr)
120
+ if expr =~ /^[^+]*-/
121
+ start = Version.parse("#{expr}.0")
122
+ else
123
+ start = process_loose_expr(expr).last.send(:first_prerelease)
124
+ end
125
+
126
+ self.new(start, SemanticPuppet::Version::MAX)
127
+ end
128
+
129
+ # Returns a range covering all versions greater than or equal to the given
130
+ # `expr`.
131
+ #
132
+ # @param expr [String] the version to be greater than or equal to
133
+ # @return [VersionRange] a range covering all versions greater than or
134
+ # equal to the given `expr`
135
+ def parse_gte_expression(expr)
136
+ if expr =~ /^[^+]*-/
137
+ start = Version.parse(expr)
138
+ else
139
+ start = process_loose_expr(expr).first.send(:first_prerelease)
140
+ end
141
+
142
+ self.new(start, SemanticPuppet::Version::MAX)
143
+ end
144
+
145
+ # Returns a range covering all versions less than the given `expr`.
146
+ #
147
+ # @param expr [String] the version to be less than
148
+ # @return [VersionRange] a range covering all versions less than the
149
+ # given `expr`
150
+ def parse_lt_expression(expr)
151
+ if expr =~ /^[^+]*-/
152
+ finish = Version.parse(expr)
153
+ else
154
+ finish = process_loose_expr(expr).first.send(:first_prerelease)
155
+ end
156
+
157
+ self.new(SemanticPuppet::Version::MIN, finish, true)
158
+ end
159
+
160
+ # Returns a range covering all versions less than or equal to the given
161
+ # `expr`.
162
+ #
163
+ # @param expr [String] the version to be less than or equal to
164
+ # @return [VersionRange] a range covering all versions less than or equal
165
+ # to the given `expr`
166
+ def parse_lte_expression(expr)
167
+ if expr =~ /^[^+]*-/
168
+ finish = Version.parse(expr)
169
+ self.new(SemanticPuppet::Version::MIN, finish)
170
+ else
171
+ finish = process_loose_expr(expr).last.send(:first_prerelease)
172
+ self.new(SemanticPuppet::Version::MIN, finish, true)
173
+ end
174
+ end
175
+
176
+ # The "reasonably close" expression is used to designate ranges that have
177
+ # a reasonable proximity to the given "loose" version number. These take
178
+ # the form:
179
+ #
180
+ # ~[Version]
181
+ #
182
+ # The general semantics of these expressions are that the given version
183
+ # forms a lower bound for the range, and the upper bound is either the
184
+ # next version number increment (at whatever precision the expression
185
+ # provides) or the next stable version (in the case of a prerelease
186
+ # version).
187
+ #
188
+ # @example "Reasonably close" major version
189
+ # "~1" # => (>=1.0.0 <2.0.0)
190
+ # @example "Reasonably close" minor version
191
+ # "~1.2" # => (>=1.2.0 <1.3.0)
192
+ # @example "Reasonably close" patch version
193
+ # "~1.2.3" # => (>=1.2.3 <1.3.0)
194
+ # @example "Reasonably close" prerelease version
195
+ # "~1.2.3-alpha" # => (>=1.2.3-alpha <1.2.4)
196
+ #
197
+ # @param expr [String] a "loose" expression to build the range around
198
+ # @return [VersionRange] a "reasonably close" version range
199
+ def parse_reasonably_close_expression(expr)
200
+ parsed, succ = process_loose_expr(expr)
201
+
202
+ if parsed.stable?
203
+ parsed = parsed.send(:first_prerelease)
204
+
205
+ # Handle the special case of "~1.2.3" expressions.
206
+ succ = succ.next(:minor) if ((parsed.major == succ.major) && (parsed.minor == succ.minor))
207
+
208
+ succ = succ.send(:first_prerelease)
209
+ self.new(parsed, succ, true)
210
+ else
211
+ self.new(parsed, succ.next(:patch).send(:first_prerelease), true)
212
+ end
213
+ end
214
+
215
+ # An "inclusive range" expression takes two version numbers (or partial
216
+ # version numbers) and creates a range that covers all versions between
217
+ # them. These take the form:
218
+ #
219
+ # [Version] - [Version]
220
+ #
221
+ # @param start [String] a "loose" expresssion for the start of the range
222
+ # @param finish [String] a "loose" expression for the end of the range
223
+ # @return [VersionRange] a {VersionRange} covering `start` to `finish`
224
+ def parse_inclusive_range_expression(start, finish)
225
+ start, _ = process_loose_expr(start)
226
+ _, finish = process_loose_expr(finish)
227
+
228
+ start = start.send(:first_prerelease) if start.stable?
229
+ if finish.stable?
230
+ exclude = true
231
+ finish = finish.send(:first_prerelease)
232
+ end
233
+
234
+ self.new(start, finish, exclude)
235
+ end
236
+
237
+ # A "loose expression" is one that takes the form of all or part of a
238
+ # valid Semantic Version number. Particularly:
239
+ #
240
+ # * [Major].[Minor].[Patch]-[Prerelease]
241
+ # * [Major].[Minor].[Patch]
242
+ # * [Major].[Minor]
243
+ # * [Major]
244
+ #
245
+ # Various placeholders are also permitted in "loose expressions"
246
+ # (typically an 'x' or an asterisk).
247
+ #
248
+ # This method parses these expressions into a minimal and maximal version
249
+ # number pair.
250
+ #
251
+ # @todo Stabilize whether the second value is inclusive or exclusive
252
+ #
253
+ # @param expr [String] a string containing a "loose" version expression
254
+ # @return [(VersionNumber, VersionNumber)] a minimal and maximal
255
+ # version pair for the given expression
256
+ def process_loose_expr(expr)
257
+ case expr
258
+ when /^(\d+)(?:[.][xX*])?$/
259
+ expr = "#{$1}.0.0"
260
+ arity = :major
261
+ when /^(\d+[.]\d+)(?:[.][xX*])?$/
262
+ expr = "#{$1}.0"
263
+ arity = :minor
264
+ when /^\d+[.]\d+[.]\d+$/
265
+ arity = :patch
266
+ end
267
+
268
+ version = next_version = Version.parse(expr)
269
+
270
+ if arity
271
+ next_version = version.next(arity)
272
+ end
273
+
274
+ [ version, next_version ]
275
+ end
276
+ end
277
+
278
+ # Computes the intersection of a pair of ranges. If the ranges have no
279
+ # useful intersection, an empty range is returned.
280
+ #
281
+ # @param other [VersionRange] the range to intersect with
282
+ # @return [VersionRange] the common subset
283
+ def intersection(other)
284
+ raise NOT_A_VERSION_RANGE unless other.kind_of?(VersionRange)
285
+
286
+ if self.begin < other.begin
287
+ return other.intersection(self)
288
+ end
289
+
290
+ unless include?(other.begin) || other.include?(self.begin)
291
+ return EMPTY_RANGE
292
+ end
293
+
294
+ endpoint = ends_before?(other) ? self : other
295
+ VersionRange.new(self.begin, endpoint.end, endpoint.exclude_end?)
296
+ end
297
+ alias :& :intersection
298
+
299
+ # Returns a string representation of this range, prefering simple common
300
+ # expressions for comprehension.
301
+ #
302
+ # @return [String] a range expression representing this VersionRange
303
+ def to_s
304
+ start, finish = self.begin, self.end
305
+ inclusive = exclude_end? ? '' : '='
306
+
307
+ case
308
+ when EMPTY_RANGE == self
309
+ "<0.0.0"
310
+ when exact_version?, patch_version?
311
+ "#{ start }"
312
+ when minor_version?
313
+ "#{ start }".sub(/.0$/, '.x')
314
+ when major_version?
315
+ "#{ start }".sub(/.0.0$/, '.x')
316
+ when open_end? && start.to_s =~ /-.*[.]0$/
317
+ ">#{ start }".sub(/.0$/, '')
318
+ when open_end?
319
+ ">=#{ start }"
320
+ when open_begin?
321
+ "<#{ inclusive }#{ finish }"
322
+ else
323
+ ">=#{ start } <#{ inclusive }#{ finish }"
324
+ end
325
+ end
326
+ alias :inspect :to_s
327
+
328
+ private
329
+
330
+ # Determines whether this {VersionRange} has an earlier endpoint than the
331
+ # give `other` range.
332
+ #
333
+ # @param other [VersionRange] the range to compare against
334
+ # @return [Boolean] true if the endpoint for this range is less than or
335
+ # equal to the endpoint of the `other` range.
336
+ def ends_before?(other)
337
+ self.end < other.end || (self.end == other.end && self.exclude_end?)
338
+ end
339
+
340
+ # Describes whether this range has an upper limit.
341
+ # @return [Boolean] true if this range has no upper limit
342
+ def open_end?
343
+ self.end == SemanticPuppet::Version::MAX
344
+ end
345
+
346
+ # Describes whether this range has a lower limit.
347
+ # @return [Boolean] true if this range has no lower limit
348
+ def open_begin?
349
+ self.begin == SemanticPuppet::Version::MIN
350
+ end
351
+
352
+ # Describes whether this range follows the patterns for matching all
353
+ # releases with the same exact version.
354
+ # @return [Boolean] true if this range matches only a single exact version
355
+ def exact_version?
356
+ self.begin == self.end
357
+ end
358
+
359
+ # Describes whether this range follows the patterns for matching all
360
+ # releases with the same major version.
361
+ # @return [Boolean] true if this range matches only a single major version
362
+ def major_version?
363
+ start, finish = self.begin, self.end
364
+
365
+ exclude_end? &&
366
+ start.major.next == finish.major &&
367
+ same_minor? && start.minor == 0 &&
368
+ same_patch? && start.patch == 0 &&
369
+ [start.prerelease, finish.prerelease] == ['', '']
370
+ end
371
+
372
+ # Describes whether this range follows the patterns for matching all
373
+ # releases with the same minor version.
374
+ # @return [Boolean] true if this range matches only a single minor version
375
+ def minor_version?
376
+ start, finish = self.begin, self.end
377
+
378
+ exclude_end? &&
379
+ same_major? &&
380
+ start.minor.next == finish.minor &&
381
+ same_patch? && start.patch == 0 &&
382
+ [start.prerelease, finish.prerelease] == ['', '']
383
+ end
384
+
385
+ # Describes whether this range follows the patterns for matching all
386
+ # releases with the same patch version.
387
+ # @return [Boolean] true if this range matches only a single patch version
388
+ def patch_version?
389
+ start, finish = self.begin, self.end
390
+
391
+ exclude_end? &&
392
+ same_major? &&
393
+ same_minor? &&
394
+ start.patch.next == finish.patch &&
395
+ [start.prerelease, finish.prerelease] == ['', '']
396
+ end
397
+
398
+ # @return [Boolean] true if `begin` and `end` share the same major verion
399
+ def same_major?
400
+ self.begin.major == self.end.major
401
+ end
402
+
403
+ # @return [Boolean] true if `begin` and `end` share the same minor verion
404
+ def same_minor?
405
+ self.begin.minor == self.end.minor
406
+ end
407
+
408
+ # @return [Boolean] true if `begin` and `end` share the same patch verion
409
+ def same_patch?
410
+ self.begin.patch == self.end.patch
411
+ end
412
+
413
+ undef :to_a
414
+
415
+ NOT_A_VERSION_RANGE = ArgumentError.new("value must be a #{VersionRange}")
416
+
417
+ public
418
+
419
+ # A range that matches no versions
420
+ EMPTY_RANGE = VersionRange.parse('< 0.0.0').freeze
421
+ end
422
+ end