semantic_puppet 0.1.3 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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