semantic_puppet 0.1.3 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,422 +1,733 @@
1
1
  require 'semantic_puppet'
2
2
 
3
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)
4
+ # A Semantic Version Range.
5
+ #
6
+ # @see https://github.com/npm/node-semver for full specification
7
+ # @api public
8
+ class VersionRange
9
+ UPPER_X = 'X'.freeze
10
+ LOWER_X = 'x'.freeze
11
+ STAR = '*'.freeze
12
+
13
+ NR = '0|[1-9][0-9]*'.freeze
14
+ XR = '(x|X|\*|' + NR + ')'.freeze
15
+ XR_NC = '(?:x|X|\*|' + NR + ')'.freeze
16
+
17
+ PART = '(?:[0-9A-Za-z-]+)'.freeze
18
+ PARTS = PART + '(?:\.' + PART + ')*'.freeze
19
+ QUALIFIER = '(?:-(' + PARTS + '))?(?:\+(' + PARTS + '))?'.freeze
20
+ QUALIFIER_NC = '(?:-' + PARTS + ')?(?:\+' + PARTS + ')?'.freeze
21
+
22
+ PARTIAL = XR_NC + '(?:\.' + XR_NC + '(?:\.' + XR_NC + QUALIFIER_NC + ')?)?'.freeze
23
+
24
+ # The ~> isn't in the spec but allowed
25
+ SIMPLE = '([<>=~^]|<=|>=|~>|~=)?(' + PARTIAL + ')'.freeze
26
+ SIMPLE_EXPR = /\A#{SIMPLE}\z/.freeze
27
+
28
+ SIMPLE_WITH_EXTRA_WS = '([<>=~^]|<=|>=)?\s+(' + PARTIAL + ')'.freeze
29
+ SIMPLE_WITH_EXTRA_WS_EXPR = /\A#{SIMPLE_WITH_EXTRA_WS}\z/.freeze
30
+
31
+ HYPHEN = '(' + PARTIAL + ')\s+-\s+(' + PARTIAL + ')'.freeze
32
+ HYPHEN_EXPR = /\A#{HYPHEN}\z/.freeze
33
+
34
+ PARTIAL_EXPR = /\A#{XR}(?:\.#{XR}(?:\.#{XR}#{QUALIFIER})?)?\z/.freeze
35
+
36
+ LOGICAL_OR = /\s*\|\|\s*/.freeze
37
+ RANGE_SPLIT = /\s+/.freeze
38
+
39
+ # Parses a version range string into a comparable {VersionRange} instance.
40
+ #
41
+ # Currently parsed version range string may take any of the following:
42
+ # forms:
43
+ #
44
+ # * Regular Semantic Version strings
45
+ # * ex. `"1.0.0"`, `"1.2.3-pre"`
46
+ # * Partial Semantic Version strings
47
+ # * ex. `"1.0.x"`, `"1"`, `"2.X"`, `"3.*"`,
48
+ # * Inequalities
49
+ # * ex. `"> 1.0.0"`, `"<3.2.0"`, `">=4.0.0"`
50
+ # * Approximate Caret Versions
51
+ # * ex. `"^1"`, `"^3.2"`, `"^4.1.0"`
52
+ # * Approximate Tilde Versions
53
+ # * ex. `"~1.0.0"`, `"~ 3.2.0"`, `"~4.0.0"`
54
+ # * Inclusive Ranges
55
+ # * ex. `"1.0.0 - 1.3.9"`
56
+ # * Range Intersections
57
+ # * ex. `">1.0.0 <=2.3.0"`
58
+ # * Combined ranges
59
+ # * ex, `">=1.0.0 <2.3.0 || >=2.5.0 <3.0.0"`
60
+ #
61
+ # @param range_string [String] the version range string to parse
62
+ # @return [VersionRange] a new {VersionRange} instance
63
+ # @api public
64
+ def self.parse(range_string)
65
+ # Remove extra whitespace after operators. Such whitespace should not cause a split
66
+ range_set = range_string.gsub(/([><=~^])(?:\s+|\s*v)/, '\1')
67
+ ranges = range_set.split(LOGICAL_OR)
68
+ return ALL_RANGE if ranges.empty?
69
+
70
+ new(ranges.map do |range|
71
+ if range =~ HYPHEN_EXPR
72
+ MinMaxRange.create(GtEqRange.new(parse_version($1)), LtEqRange.new(parse_version($2)))
44
73
  else
45
- raise ArgumentError
74
+ # Split on whitespace
75
+ simples = range.split(RANGE_SPLIT).map do |simple|
76
+ match_data = SIMPLE_EXPR.match(simple)
77
+ raise ArgumentError, "Unparsable version range: \"#{range_string}\"" unless match_data
78
+ operand = match_data[2]
79
+
80
+ # Case based on operator
81
+ case match_data[1]
82
+ when '~', '~>', '~='
83
+ parse_tilde(operand)
84
+ when '^'
85
+ parse_caret(operand)
86
+ when '>'
87
+ parse_gt_version(operand)
88
+ when '>='
89
+ GtEqRange.new(parse_version(operand))
90
+ when '<'
91
+ LtRange.new(parse_version(operand))
92
+ when '<='
93
+ parse_lteq_version(operand)
94
+ when '='
95
+ parse_xrange(operand)
96
+ else
97
+ parse_xrange(operand)
98
+ end
99
+ end
100
+ simples.size == 1 ? simples[0] : MinMaxRange.create(*simples)
46
101
  end
102
+ end.uniq, range_string).freeze
103
+ end
47
104
 
48
- rescue ArgumentError
49
- raise ArgumentError, "Unparsable version range: #{range_str.inspect}"
50
- end
105
+ def self.parse_partial(expr)
106
+ match_data = PARTIAL_EXPR.match(expr)
107
+ raise ArgumentError, "Unparsable version range: \"#{expr}\"" unless match_data
108
+ match_data
109
+ end
110
+ private_class_method :parse_partial
51
111
 
52
- private
112
+ def self.parse_caret(expr)
113
+ match_data = parse_partial(expr)
114
+ major = digit(match_data[1])
115
+ major == 0 ? allow_patch_updates(major, match_data) : allow_minor_updates(major, match_data)
116
+ end
117
+ private_class_method :parse_caret
53
118
 
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
119
+ def self.parse_tilde(expr)
120
+ match_data = parse_partial(expr)
121
+ allow_patch_updates(digit(match_data[1]), match_data)
122
+ end
123
+ private_class_method :parse_tilde
61
124
 
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)
125
+ def self.parse_xrange(expr)
126
+ match_data = parse_partial(expr)
127
+ allow_patch_updates(digit(match_data[1]), match_data, false)
128
+ end
129
+ private_class_method :parse_xrange
71
130
 
72
- if start.stable?
73
- start = start.send(:first_prerelease)
74
- end
131
+ def self.allow_patch_updates(major, match_data, tilde_or_caret = true)
132
+ return AllRange::SINGLETON unless major
75
133
 
76
- if finish.stable?
77
- exclude = true
78
- finish = finish.send(:first_prerelease)
79
- end
134
+ minor = digit(match_data[2])
135
+ return MinMaxRange.new(GtEqRange.new(Version.new(major, 0, 0)), LtRange.new(Version.new(major + 1, 0, 0))) unless minor
80
136
 
81
- self.new(start, finish, exclude)
82
- end
137
+ patch = digit(match_data[3])
138
+ return MinMaxRange.new(GtEqRange.new(Version.new(major, minor, 0)), LtRange.new(Version.new(major, minor + 1, 0))) unless patch
83
139
 
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
140
+ version = Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5]))
141
+ return EqRange.new(version) unless tilde_or_caret
113
142
 
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")
143
+ MinMaxRange.new(GtEqRange.new(version), LtRange.new(Version.new(major, minor + 1, 0)))
144
+ end
145
+ private_class_method :allow_patch_updates
146
+
147
+ def self.allow_minor_updates(major, match_data)
148
+ return AllRange.SINGLETON unless major
149
+ minor = digit(match_data[2])
150
+ if minor
151
+ patch = digit(match_data[3])
152
+ if patch.nil?
153
+ MinMaxRange.new(GtEqRange.new(Version.new(major, minor, 0)), LtRange.new(Version.new(major + 1, 0, 0)))
122
154
  else
123
- start = process_loose_expr(expr).last.send(:first_prerelease)
155
+ if match_data[4].nil?
156
+ MinMaxRange.new(GtEqRange.new(Version.new(major, minor, patch)), LtRange.new(Version.new(major + 1, 0, 0)))
157
+ else
158
+ MinMaxRange.new(
159
+ GtEqRange.new(
160
+ Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5]))),
161
+ LtRange.new(Version.new(major + 1, 0, 0)))
162
+ end
124
163
  end
125
-
126
- self.new(start, SemanticPuppet::Version::MAX)
164
+ else
165
+ MinMaxRange.new(GtEqRange.new(Version.new(major, 0, 0)), LtRange.new(Version.new(major + 1, 0, 0)))
127
166
  end
167
+ end
168
+ private_class_method :allow_minor_updates
128
169
 
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)
170
+ def self.digit(str)
171
+ (str.nil? || UPPER_X == str || LOWER_X == str || STAR == str) ? nil : str.to_i
172
+ end
173
+ private_class_method :digit
174
+
175
+ def self.parse_version(expr)
176
+ match_data = parse_partial(expr)
177
+ major = digit(match_data[1]) || 0
178
+ minor = digit(match_data[2]) || 0
179
+ patch = digit(match_data[3]) || 0
180
+ Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5]))
181
+ end
182
+ private_class_method :parse_version
183
+
184
+ def self.parse_gt_version(expr)
185
+ match_data = parse_partial(expr)
186
+ major = digit(match_data[1])
187
+ return LtRange::MATCH_NOTHING unless major
188
+ minor = digit(match_data[2])
189
+ return GtEqRange.new(Version.new(major + 1, 0, 0)) unless minor
190
+ patch = digit(match_data[3])
191
+ return GtEqRange.new(Version.new(major, minor + 1, 0)) unless patch
192
+ return GtRange.new(Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5])))
193
+ end
194
+ private_class_method :parse_gt_version
195
+
196
+ def self.parse_lteq_version(expr)
197
+ match_data = parse_partial(expr)
198
+ major = digit(match_data[1])
199
+ return AllRange.SINGLETON unless major
200
+ minor = digit(match_data[2])
201
+ return LtRange.new(Version.new(major + 1, 0, 0)) unless minor
202
+ patch = digit(match_data[3])
203
+ return LtRange.new(Version.new(major, minor + 1, 0)) unless patch
204
+ return LtEqRange.new(Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5])))
205
+ end
206
+ private_class_method :parse_lteq_version
207
+
208
+ # Provides read access to the ranges. For internal use only
209
+ # @api private
210
+ attr_reader :ranges
211
+
212
+ # Creates a new version range
213
+ # @overload initialize(from, to, exclude_end = false)
214
+ # Creates a new instance using ruby `Range` semantics
215
+ # @param begin [String,Version] the version denoting the start of the range (always inclusive)
216
+ # @param end [String,Version] the version denoting the end of the range
217
+ # @param exclude_end [Boolean] `true` if the `end` version should be excluded from the range
218
+ # @overload initialize(ranges, string)
219
+ # Creates a new instance based on parsed content. For internal use only
220
+ # @param ranges [Array<AbstractRange>] the ranges to include in this range
221
+ # @param string [String] the original string representation that was parsed to produce the ranges
222
+ #
223
+ # @api private
224
+ def initialize(ranges, string, exclude_end = false)
225
+ unless ranges.is_a?(Array)
226
+ lb = GtEqRange.new(ranges)
227
+ if exclude_end
228
+ ub = LtRange.new(string)
229
+ string = ">=#{string} <#{ranges}"
138
230
  else
139
- start = process_loose_expr(expr).first.send(:first_prerelease)
231
+ ub = LtEqRange.new(string)
232
+ string = "#{string} - #{ranges}"
140
233
  end
234
+ ranges = [MinMaxRange.create(lb, ub)]
235
+ end
236
+ ranges.compact!
237
+
238
+ merge_happened = true
239
+ while ranges.size > 1 && merge_happened
240
+ # merge ranges if possible
241
+ merge_happened = false
242
+ result = []
243
+ until ranges.empty?
244
+ unmerged = []
245
+ x = ranges.pop
246
+ result << ranges.reduce(x) do |memo, y|
247
+ merged = memo.merge(y)
248
+ if merged.nil?
249
+ unmerged << y
250
+ else
251
+ merge_happened = true
252
+ memo = merged
253
+ end
254
+ memo
255
+ end
256
+ ranges = unmerged
257
+ end
258
+ ranges = result.reverse!
259
+ end
260
+
261
+ ranges = [LtRange::MATCH_NOTHING] if ranges.empty?
262
+ @ranges = ranges
263
+ @string = string.nil? ? ranges.join(' || ') : string
264
+ end
265
+
266
+ def eql?(range)
267
+ range.is_a?(VersionRange) && @ranges.eql?(range.ranges)
268
+ end
269
+ alias == eql?
270
+
271
+ def hash
272
+ @ranges.hash
273
+ end
274
+
275
+ # Returns the version that denotes the beginning of this range.
276
+ #
277
+ # Since this really is an OR between disparate ranges, it may have multiple beginnings. This method
278
+ # returns `nil` if that is the case.
279
+ #
280
+ # @return [Version] the beginning of the range, or `nil` if there are multiple beginnings
281
+ # @api public
282
+ def begin
283
+ @ranges.size == 1 ? @ranges[0].begin : nil
284
+ end
141
285
 
142
- self.new(start, SemanticPuppet::Version::MAX)
286
+ # Returns the version that denotes the end of this range.
287
+ #
288
+ # Since this really is an OR between disparate ranges, it may have multiple ends. This method
289
+ # returns `nil` if that is the case.
290
+ #
291
+ # @return [Version] the end of the range, or `nil` if there are multiple ends
292
+ # @api public
293
+ def end
294
+ @ranges.size == 1 ? @ranges[0].end : nil
295
+ end
296
+
297
+ # Returns `true` if the beginning is excluded from the range.
298
+ #
299
+ # Since this really is an OR between disparate ranges, it may have multiple beginnings. This method
300
+ # returns `nil` if that is the case.
301
+ #
302
+ # @return [Boolean] `true` if the beginning is excluded from the range, `false` if included, or `nil` if there are multiple beginnings
303
+ # @api public
304
+ def exclude_begin?
305
+ @ranges.size == 1 ? @ranges[0].exclude_begin? : nil
306
+ end
307
+
308
+ # Returns `true` if the end is excluded from the range.
309
+ #
310
+ # Since this really is an OR between disparate ranges, it may have multiple ends. This method
311
+ # returns `nil` if that is the case.
312
+ #
313
+ # @return [Boolean] `true` if the end is excluded from the range, `false` if not, or `nil` if there are multiple ends
314
+ # @api public
315
+ def exclude_end?
316
+ @ranges.size == 1 ? @ranges[0].exclude_end? : nil
317
+ end
318
+
319
+ # @return [Boolean] `true` if the given version is included in the range
320
+ # @api public
321
+ def include?(version)
322
+ @ranges.any? { |range| range.include?(version) && (version.stable? || range.test_prerelease?(version)) }
323
+ end
324
+ alias member? include?
325
+ alias cover? include?
326
+ alias === include?
327
+
328
+ # Computes the intersection of a pair of ranges. If the ranges have no
329
+ # useful intersection, an empty range is returned.
330
+ #
331
+ # @param other [VersionRange] the range to intersect with
332
+ # @return [VersionRange] the common subset
333
+ # @api public
334
+ def intersection(other)
335
+ raise ArgumentError, "value must be a #{self.class.name}" unless other.is_a?(VersionRange)
336
+ result = @ranges.map { |range| other.ranges.map { |o_range| range.intersection(o_range) } }.flatten
337
+ result.compact!
338
+ result.uniq!
339
+ result.empty? ? EMPTY_RANGE : VersionRange.new(result, nil)
340
+ end
341
+ alias :& :intersection
342
+
343
+ # Returns a string representation of this range. This will be the string that was used
344
+ # when the range was parsed.
345
+ #
346
+ # @return [String] a range expression representing this VersionRange
347
+ # @api public
348
+ def to_s
349
+ @string
350
+ end
351
+
352
+ # Returns a canonical string representation of this range, assembled from the internal
353
+ # matchers.
354
+ #
355
+ # @return [String] a range expression representing this VersionRange
356
+ # @api public
357
+ def inspect
358
+ @ranges.join(' || ')
359
+ end
360
+
361
+ # @api private
362
+ class AbstractRange
363
+ def include?(_)
364
+ true
143
365
  end
144
366
 
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
367
+ def begin
368
+ Version::MIN
369
+ end
156
370
 
157
- self.new(SemanticPuppet::Version::MIN, finish, true)
371
+ def end
372
+ Version::MAX
158
373
  end
159
374
 
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
375
+ def exclude_begin?
376
+ false
174
377
  end
175
378
 
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)
379
+ def exclude_end?
380
+ false
381
+ end
201
382
 
202
- if parsed.stable?
203
- parsed = parsed.send(:first_prerelease)
383
+ def eql?(other)
384
+ other.class.eql?(self.class)
385
+ end
204
386
 
205
- # Handle the special case of "~1.2.3" expressions.
206
- succ = succ.next(:minor) if ((parsed.major == succ.major) && (parsed.minor == succ.minor))
387
+ def ==(other)
388
+ eql?(other)
389
+ end
207
390
 
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
391
+ def lower_bound?
392
+ false
393
+ end
394
+
395
+ def upper_bound?
396
+ false
213
397
  end
214
398
 
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:
399
+ # Merge two ranges so that the result matches the intersection of all matching versions.
218
400
  #
219
- # [Version] - [Version]
401
+ # @param range [AbastractRange] the range to intersect with
402
+ # @return [AbastractRange,nil] the intersection between the ranges
220
403
  #
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)
404
+ # @api private
405
+ def intersection(range)
406
+ cmp = self.begin <=> range.end
407
+ if cmp > 0
408
+ nil
409
+ elsif cmp == 0
410
+ exclude_begin? || range.exclude_end? ? nil : EqRange.new(self.begin)
411
+ else
412
+ cmp = range.begin <=> self.end
413
+ if cmp > 0
414
+ nil
415
+ elsif cmp == 0
416
+ range.exclude_begin? || exclude_end? ? nil : EqRange.new(range.begin)
417
+ else
418
+ cmp = self.begin <=> range.begin
419
+ min = if cmp < 0
420
+ range
421
+ elsif cmp > 0
422
+ self
423
+ else
424
+ self.exclude_begin? ? self : range
425
+ end
426
+
427
+ cmp = self.end <=> range.end
428
+ max = if cmp > 0
429
+ range
430
+ elsif cmp < 0
431
+ self
432
+ else
433
+ self.exclude_end? ? self : range
434
+ end
435
+
436
+ if !max.upper_bound?
437
+ min
438
+ elsif !min.lower_bound?
439
+ max
440
+ else
441
+ MinMaxRange.new(min, max)
442
+ end
443
+ end
232
444
  end
233
-
234
- self.new(start, finish, exclude)
235
445
  end
236
446
 
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]
447
+ # Merge two ranges so that the result matches the sum of all matching versions. A merge
448
+ # is only possible when the ranges are either adjacent or have an overlap.
244
449
  #
245
- # Various placeholders are also permitted in "loose expressions"
246
- # (typically an 'x' or an asterisk).
450
+ # @param other [AbastractRange] the range to merge with
451
+ # @return [AbastractRange,nil] the result of the merge
247
452
  #
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
453
+ # @api private
454
+ def merge(other)
455
+ if include?(other.begin) || other.include?(self.begin)
456
+ cmp = self.begin <=> other.begin
457
+ if cmp < 0
458
+ min = self.begin
459
+ excl_begin = exclude_begin?
460
+ elsif cmp > 0
461
+ min = other.begin
462
+ excl_begin = other.exclude_begin?
463
+ else
464
+ min = self.begin
465
+ excl_begin = exclude_begin? && other.exclude_begin?
466
+ end
467
+
468
+ cmp = self.end <=> other.end
469
+ if cmp > 0
470
+ max = self.end
471
+ excl_end = self.exclude_end?
472
+ elsif cmp < 0
473
+ max = other.end
474
+ excl_end = other.exclude_end?
475
+ else
476
+ max = self.end
477
+ excl_end = exclude_end && other.exclude_end?
478
+ end
479
+
480
+ MinMaxRange.create(excl_begin ? GtRange.new(min) : GtEqRange.new(min), excl_end ? LtRange.new(max) : LtEqRange.new(max))
481
+ elsif exclude_end? && !other.exclude_begin? && self.end == other.begin
482
+ # Adjacent, self before other
483
+ from_to(self, other)
484
+ elsif other.exclude_end? && !exclude_begin? && other.end == self.begin
485
+ # Adjacent, other before self
486
+ from_to(other, self)
487
+ elsif !exclude_end? && !other.exclude_begin? && self.end.next(:patch) == other.begin
488
+ # Adjacent, self before other
489
+ from_to(self, other)
490
+ elsif !other.exclude_end? && !exclude_begin? && other.end.next(:patch) == self.begin
491
+ # Adjacent, other before self
492
+ from_to(other, self)
493
+ else
494
+ # No overlap
495
+ nil
266
496
  end
497
+ end
267
498
 
268
- version = next_version = Version.parse(expr)
499
+ # Checks if this matcher accepts a prerelease with the same major, minor, patch triple as the given version. Only matchers
500
+ # where this has been explicitly stated will respond `true` to this method
501
+ #
502
+ # @return [Boolean] `true` if this matcher accepts a prerelase with the tuple from the given version
503
+ def test_prerelease?(_)
504
+ false
505
+ end
269
506
 
270
- if arity
271
- next_version = version.next(arity)
272
- end
507
+ private
273
508
 
274
- [ version, next_version ]
509
+ def from_to(a, b)
510
+ MinMaxRange.create(a.exclude_begin? ? GtRange.new(a.begin) : GtEqRange.new(a.begin), b.exclude_end? ? LtRange.new(b.end) : LtEqRange.new(b.end))
275
511
  end
276
512
  end
277
513
 
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)
514
+ # @api private
515
+ class AllRange < AbstractRange
516
+ SINGLETON = AllRange.new
285
517
 
286
- if self.begin < other.begin
287
- return other.intersection(self)
518
+ def intersection(range)
519
+ range
288
520
  end
289
521
 
290
- unless include?(other.begin) || other.include?(self.begin)
291
- return EMPTY_RANGE
522
+ def merge(range)
523
+ self
292
524
  end
293
525
 
294
- endpoint = ends_before?(other) ? self : other
295
- VersionRange.new(self.begin, endpoint.end, endpoint.exclude_end?)
296
- end
297
- alias :& :intersection
526
+ def test_prerelease?(_)
527
+ true
528
+ end
298
529
 
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 }"
530
+ def to_s
531
+ '*'
324
532
  end
325
533
  end
326
- alias :inspect :to_s
327
534
 
328
- private
535
+ # @api private
536
+ class MinMaxRange < AbstractRange
537
+ attr_reader :min, :max
329
538
 
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
539
+ def self.create(*ranges)
540
+ ranges.reduce { |memo, range| memo.intersection(range) }
541
+ end
339
542
 
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
543
+ def initialize(min, max)
544
+ @min = min.is_a?(MinMaxRange) ? min.min : min
545
+ @max = max.is_a?(MinMaxRange) ? max.max : max
546
+ end
345
547
 
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
548
+ def begin
549
+ @min.begin
550
+ end
351
551
 
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
552
+ def end
553
+ @max.end
554
+ end
555
+
556
+ def exclude_begin?
557
+ @min.exclude_begin?
558
+ end
559
+
560
+ def exclude_end?
561
+ @max.exclude_end?
562
+ end
563
+
564
+ def eql?(other)
565
+ super && @min.eql?(other.min) && @max.eql?(other.max)
566
+ end
567
+
568
+ def hash
569
+ @min.hash ^ @max.hash
570
+ end
571
+
572
+ def include?(version)
573
+ @min.include?(version) && @max.include?(version)
574
+ end
575
+
576
+ def lower_bound?
577
+ @min.lower_bound?
578
+ end
579
+
580
+ def upper_bound?
581
+ @max.upper_bound?
582
+ end
583
+
584
+ def test_prerelease?(version)
585
+ @min.test_prerelease?(version) || @max.test_prerelease?(version)
586
+ end
358
587
 
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] == ['', '']
588
+ def to_s
589
+ "#{@min} #{@max}"
590
+ end
591
+ alias inspect to_s
370
592
  end
371
593
 
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] == ['', '']
594
+ # @api private
595
+ class ComparatorRange < AbstractRange
596
+ attr_reader :version
597
+
598
+ def initialize(version)
599
+ @version = version
600
+ end
601
+
602
+ def eql?(other)
603
+ super && @version.eql?(other.version)
604
+ end
605
+
606
+ def hash
607
+ @class.hash ^ @version.hash
608
+ end
609
+
610
+ # Checks if this matcher accepts a prerelease with the same major, minor, patch triple as the given version
611
+ def test_prerelease?(version)
612
+ !@version.stable? && @version.major == version.major && @version.minor == version.minor && @version.patch == version.patch
613
+ end
383
614
  end
384
615
 
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] == ['', '']
616
+ # @api private
617
+ class GtRange < ComparatorRange
618
+ def include?(version)
619
+ version > @version
620
+ end
621
+
622
+ def exclude_begin?
623
+ true
624
+ end
625
+
626
+ def begin
627
+ @version
628
+ end
629
+
630
+ def lower_bound?
631
+ true
632
+ end
633
+
634
+ def to_s
635
+ ">#{@version}"
636
+ end
396
637
  end
397
638
 
398
- # @return [Boolean] true if `begin` and `end` share the same major verion
399
- def same_major?
400
- self.begin.major == self.end.major
639
+ # @api private
640
+ class GtEqRange < ComparatorRange
641
+ def include?(version)
642
+ version >= @version
643
+ end
644
+
645
+ def begin
646
+ @version
647
+ end
648
+
649
+ def lower_bound?
650
+ @version != Version::MIN
651
+ end
652
+
653
+ def to_s
654
+ ">=#{@version}"
655
+ end
401
656
  end
402
657
 
403
- # @return [Boolean] true if `begin` and `end` share the same minor verion
404
- def same_minor?
405
- self.begin.minor == self.end.minor
658
+ # @api private
659
+ class LtRange < ComparatorRange
660
+ MATCH_NOTHING = LtRange.new(Version::MIN)
661
+
662
+ def include?(version)
663
+ version < @version
664
+ end
665
+
666
+ def exclude_end?
667
+ true
668
+ end
669
+
670
+ def end
671
+ @version
672
+ end
673
+
674
+ def upper_bound?
675
+ true
676
+ end
677
+
678
+ def to_s
679
+ self.equal?(MATCH_NOTHING) ? '<0.0.0' : "<#{@version}"
680
+ end
406
681
  end
407
682
 
408
- # @return [Boolean] true if `begin` and `end` share the same patch verion
409
- def same_patch?
410
- self.begin.patch == self.end.patch
683
+ # @api private
684
+ class LtEqRange < ComparatorRange
685
+ def include?(version)
686
+ version <= @version
687
+ end
688
+
689
+ def end
690
+ @version
691
+ end
692
+
693
+ def upper_bound?
694
+ @version != Version::MAX
695
+ end
696
+
697
+ def to_s
698
+ "<=#{@version}"
699
+ end
411
700
  end
412
701
 
413
- undef :to_a
702
+ # @api private
703
+ class EqRange < ComparatorRange
704
+ def include?(version)
705
+ version == @version
706
+ end
414
707
 
415
- NOT_A_VERSION_RANGE = ArgumentError.new("value must be a #{VersionRange}")
708
+ def begin
709
+ @version
710
+ end
711
+
712
+ def lower_bound?
713
+ @version != Version::MIN
714
+ end
416
715
 
417
- public
716
+ def upper_bound?
717
+ @version != Version::MAX
718
+ end
719
+
720
+ def end
721
+ @version
722
+ end
723
+
724
+ def to_s
725
+ @version.to_s
726
+ end
727
+ end
418
728
 
419
729
  # A range that matches no versions
420
- EMPTY_RANGE = VersionRange.parse('< 0.0.0').freeze
730
+ EMPTY_RANGE = VersionRange.new([], nil).freeze
731
+ ALL_RANGE = VersionRange.new([AllRange::SINGLETON], '*')
421
732
  end
422
733
  end