vers 1.3.1 → 2.0.0

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.
@@ -5,17 +5,26 @@ require_relative 'version'
5
5
 
6
6
  module Vers
7
7
  class VersionRange
8
- attr_reader :intervals, :raw_constraints, :scheme
8
+ VALIDATED_CONTAINMENT_SCHEMES = %w[bazel cargo composer go hex npm pub pypi semver].freeze
9
9
 
10
- def initialize(intervals = [], raw_constraints: nil, scheme: nil)
11
- @scheme = scheme
12
- @intervals = intervals.select { |i| i && !i.empty? }
10
+ attr_reader :intervals, :raw_constraints, :scheme, :exclusions
11
+
12
+ def initialize(intervals = [], raw_constraints: nil, scheme: nil, exclusions: [])
13
+ canonical_scheme = Scheme.canonical(scheme)
14
+ interval_schemes = intervals.compact.map(&:scheme).compact.map { |value| Scheme.canonical(value) }.uniq
15
+ if interval_schemes.length > 1 || (canonical_scheme && interval_schemes.any? { |value| value != canonical_scheme })
16
+ raise ArgumentError, "Cannot combine different version range schemes"
17
+ end
18
+
19
+ @scheme = canonical_scheme || interval_schemes.first
20
+ @intervals = intervals.select { |interval| interval && !interval.empty? }.map { |interval| interval.with_scheme(@scheme) }
13
21
  if @scheme
14
22
  @intervals.sort! { |a, b| compare_interval_bounds(a, b) }
15
23
  else
16
24
  @intervals.sort_by! { |i| [i.min || '', i.max || ''] }
17
25
  end
18
26
  @raw_constraints = raw_constraints
27
+ @exclusions = exclusions.dup
19
28
  merge_overlapping_intervals!
20
29
  end
21
30
 
@@ -44,35 +53,206 @@ module Vers
44
53
  end
45
54
 
46
55
  def unbounded?
47
- intervals.length == 1 && intervals.first.unbounded?
56
+ exclusions.empty? && intervals.length == 1 && intervals.first.unbounded?
48
57
  end
49
58
 
50
59
  def contains?(version)
51
- intervals.any? { |interval| interval.contains?(version) }
60
+ if VALIDATED_CONTAINMENT_SCHEMES.include?(scheme) && !Version.valid?(version, scheme)
61
+ return false
62
+ end
63
+ return false if exclusions.any? { |excluded| excluded_version?(version, excluded) }
64
+
65
+ intervals.any? do |interval|
66
+ contains = if scheme == "composer"
67
+ composer_interval_contains?(interval, version)
68
+ elsif scheme == "pypi"
69
+ pypi_interval_contains?(interval, version)
70
+ else
71
+ interval.contains?(version)
72
+ end
73
+ contains && prerelease_allowed?(interval, version)
74
+ end
75
+ end
76
+
77
+ def composer_interval_contains?(interval, version)
78
+ candidate_branch = ComposerVersion.branch?(version)
79
+ minimum_branch = interval.min && ComposerVersion.branch?(interval.min)
80
+ maximum_branch = interval.max && ComposerVersion.branch?(interval.max)
81
+ return interval.contains?(version) unless candidate_branch || minimum_branch || maximum_branch
82
+ return true if interval.unbounded?
83
+
84
+ candidate_branch && minimum_branch && maximum_branch &&
85
+ interval.min_inclusive && interval.max_inclusive && interval.min == interval.max && version == interval.min
86
+ end
87
+
88
+ def pypi_interval_contains?(interval, version)
89
+ if interval.min && interval.max && interval.min_inclusive && interval.max_inclusive &&
90
+ PyPIVersion.compare(interval.min, interval.max).zero?
91
+ return PyPIVersion.specifier_equal?(version, interval.min)
92
+ end
93
+ return false unless interval.contains?(version)
94
+
95
+ candidate = PyPIVersion.parse(version)
96
+ return false unless candidate
97
+
98
+ if interval.min && !interval.min_inclusive
99
+ bound = PyPIVersion.parse(interval.min)
100
+ if bound
101
+ return false if PyPIVersion.specifier_equal?(version, interval.min)
102
+
103
+ without_post = PyPIVersion.without_post_and_dev(candidate)
104
+ if candidate.post_number && PyPIVersion.versions_equal?(without_post, bound, ignore_local: true)
105
+ return false
106
+ end
107
+ end
108
+ end
109
+
110
+ if interval.max && !interval.max_inclusive
111
+ bound = PyPIVersion.parse(interval.max)
112
+ if bound
113
+ bound_is_release = bound.pre_tag.nil? && bound.post_number.nil? && bound.dev_number.nil?
114
+ if bound_is_release && PyPIVersion.same_release?(candidate, bound) &&
115
+ (!candidate.pre_tag.nil? || !candidate.dev_number.nil?)
116
+ return false
117
+ end
118
+
119
+ without_dev = PyPIVersion.without_dev(candidate)
120
+ if candidate.dev_number && PyPIVersion.versions_equal?(without_dev, bound, ignore_local: true)
121
+ return false
122
+ end
123
+ end
124
+ end
125
+
126
+ true
127
+ end
128
+
129
+ def prerelease_allowed?(interval, version)
130
+ return true unless %w[npm cargo].include?(scheme)
131
+
132
+ candidate = SemverVersion.parse(version.to_s.strip)
133
+ return false unless candidate
134
+ return true if candidate.prerelease.empty?
135
+
136
+ [interval.min, interval.max].compact.any? do |bound|
137
+ parsed_bound = SemverVersion.parse(bound.to_s.strip)
138
+ next false unless parsed_bound && !parsed_bound.prerelease.empty?
139
+
140
+ candidate.core.zip(parsed_bound.core).all? do |candidate_part, bound_part|
141
+ VersionComparison.compare_numbers(candidate_part, bound_part).zero?
142
+ end
143
+ end
144
+ end
145
+
146
+ def excluded_version?(version, excluded)
147
+ if scheme == "pypi"
148
+ PyPIVersion.specifier_equal?(version, excluded)
149
+ elsif scheme == "composer" && (ComposerVersion.branch?(version) || ComposerVersion.branch?(excluded))
150
+ version == excluded
151
+ else
152
+ Version.compare_with_scheme(version, excluded, scheme).zero?
153
+ end
52
154
  end
53
155
 
54
156
  def intersect(other)
55
- merged_scheme = @scheme || other.scheme
157
+ merged_scheme = compatible_scheme(other)
56
158
  result_intervals = []
57
159
 
58
160
  intervals.each do |interval1|
59
161
  other.intervals.each do |interval2|
60
- intersection = interval1.intersect(interval2)
162
+ intersection = intersect_intervals(interval1, interval2, merged_scheme)
61
163
  result_intervals << intersection unless intersection.empty?
62
164
  end
63
165
  end
64
166
 
65
167
  combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals)
66
- self.class.new(result_intervals, raw_constraints: combined_raw, scheme: merged_scheme)
168
+ self.class.new(
169
+ result_intervals,
170
+ raw_constraints: combined_raw,
171
+ scheme: merged_scheme,
172
+ exclusions: exclusions + other.exclusions
173
+ )
174
+ end
175
+
176
+ def intersect_intervals(left, right, comparison_scheme)
177
+ return left.intersect(right) unless comparison_scheme == "pypi"
178
+
179
+ left = left.with_scheme(comparison_scheme)
180
+ right = right.with_scheme(comparison_scheme)
181
+ left_exact = exact_interval?(left)
182
+ right_exact = exact_interval?(right)
183
+ return left.intersect(right) unless left_exact || right_exact
184
+
185
+ if left_exact && right_exact
186
+ left_contains_right = pypi_interval_contains?(left, right.min)
187
+ right_contains_left = pypi_interval_contains?(right, left.min)
188
+ return right if left_contains_right && !right_contains_left
189
+ return left if right_contains_left
190
+ return left if left_contains_right
191
+
192
+ return Interval.empty(scheme: comparison_scheme)
193
+ end
194
+
195
+ exact = left_exact ? left : right
196
+ other_interval = left_exact ? right : left
197
+ return exact if pypi_interval_contains?(other_interval, exact.min)
198
+
199
+ Interval.empty(scheme: comparison_scheme)
200
+ end
201
+
202
+ def exact_interval?(interval)
203
+ interval.min && interval.max && interval.min_inclusive && interval.max_inclusive &&
204
+ Version.compare_for_range(interval.min, interval.max, interval.scheme || scheme).zero?
67
205
  end
68
206
 
69
207
  def union(other)
70
- merged_scheme = @scheme || other.scheme
208
+ merged_scheme = compatible_scheme(other)
71
209
  combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals)
72
- self.class.new(intervals + other.intervals, raw_constraints: combined_raw, scheme: merged_scheme)
210
+ left = with_scheme(merged_scheme)
211
+ right = other.with_scheme(merged_scheme)
212
+ combined_exclusions = []
213
+ left.exclusions.each do |excluded|
214
+ combined_exclusions << excluded unless right.contains?(excluded)
215
+ end
216
+ right.exclusions.each do |excluded|
217
+ next if left.contains?(excluded) || combined_exclusions.include?(excluded)
218
+
219
+ combined_exclusions << excluded
220
+ end
221
+ self.class.new(
222
+ intervals + other.intervals,
223
+ raw_constraints: combined_raw,
224
+ scheme: merged_scheme,
225
+ exclusions: combined_exclusions
226
+ )
227
+ end
228
+
229
+ def with_scheme(value)
230
+ canonical = Scheme.canonical(value)
231
+ if scheme && canonical && scheme != canonical
232
+ raise ArgumentError, "Cannot combine #{scheme} and #{canonical} version ranges"
233
+ end
234
+ return self if scheme == canonical
235
+
236
+ self.class.new(intervals, raw_constraints: raw_constraints, scheme: canonical, exclusions: exclusions)
237
+ end
238
+
239
+ def compatible_scheme(other)
240
+ if scheme && other.scheme && scheme != other.scheme
241
+ raise ArgumentError, "Cannot combine #{scheme} and #{other.scheme} version ranges"
242
+ end
243
+
244
+ scheme || other.scheme
73
245
  end
74
246
 
75
247
  def complement
248
+ unless exclusions.empty?
249
+ base = self.class.new(intervals, scheme: scheme).complement
250
+ exclusions.each do |excluded|
251
+ base = base.union(self.class.exact(excluded, scheme: scheme))
252
+ end
253
+ return base
254
+ end
255
+
76
256
  return self.class.unbounded(scheme: @scheme) if empty?
77
257
  return self.class.empty(scheme: @scheme) if unbounded?
78
258
 
@@ -121,37 +301,14 @@ module Vers
121
301
  end
122
302
 
123
303
  def exclude(version)
124
- return self if !contains?(version)
125
-
126
- result_intervals = []
127
-
128
- intervals.each do |interval|
129
- if interval.contains?(version)
130
- if interval.min.nil? || version_compare(interval.min, version) < 0
131
- result_intervals << Interval.new(
132
- min: interval.min,
133
- max: version,
134
- min_inclusive: interval.min_inclusive,
135
- max_inclusive: false,
136
- scheme: @scheme
137
- )
138
- end
139
-
140
- if interval.max.nil? || version_compare(version, interval.max) < 0
141
- result_intervals << Interval.new(
142
- min: version,
143
- max: interval.max,
144
- min_inclusive: false,
145
- max_inclusive: interval.max_inclusive,
146
- scheme: @scheme
147
- )
148
- end
149
- else
150
- result_intervals << interval
151
- end
152
- end
153
-
154
- self.class.new(result_intervals, raw_constraints: raw_constraints, scheme: @scheme)
304
+ return self unless contains?(version)
305
+
306
+ self.class.new(
307
+ intervals,
308
+ raw_constraints: raw_constraints,
309
+ scheme: scheme,
310
+ exclusions: exclusions + [version]
311
+ )
155
312
  end
156
313
 
157
314
  def to_s
@@ -186,11 +343,7 @@ module Vers
186
343
  return -1 if a.nil?
187
344
  return 1 if b.nil?
188
345
 
189
- if @scheme
190
- Version.compare_with_scheme(a, b, @scheme)
191
- else
192
- Version.compare(a, b)
193
- end
346
+ Version.compare_for_range(a, b, @scheme)
194
347
  end
195
348
 
196
349
  def compare_interval_bounds(a, b)
@@ -220,4 +373,4 @@ module Vers
220
373
  end
221
374
  end
222
375
  end
223
- end
376
+ end
data/lib/vers.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "vers/version"
4
4
  require_relative "vers/maven_version"
5
5
  require_relative "vers/nuget_version"
6
+ require_relative "vers/bazel_version"
6
7
  require_relative "vers/interval"
7
8
  require_relative "vers/version_range"
8
9
  require_relative "vers/constraint"
@@ -37,7 +38,7 @@ require_relative "vers/parser"
37
38
  # gem_range = Vers.parse_native("~> 1.0", "gem")
38
39
  #
39
40
  # # Check version containment
40
- # Vers.satisfies?("1.5.0", ">=1.0.0,<2.0.0") # => true
41
+ # Vers.satisfies?("1.5.0", ">=1.0.0,<2.0.0", "pypi") # => true
41
42
  #
42
43
  # # Compare versions
43
44
  # Vers.compare("1.2.3", "1.2.4") # => -1
@@ -70,7 +71,7 @@ module Vers
70
71
  # == Examples
71
72
  #
72
73
  # Vers.parse("vers:npm/>=1.2.3|<2.0.0")
73
- # Vers.parse("vers:gem/~>1.0")
74
+ # Vers.parse_native("~>1.0", "gem")
74
75
  # Vers.parse("*") # unbounded range
75
76
  #
76
77
  def self.parse(vers_string)
@@ -152,7 +153,7 @@ module Vers
152
153
  #
153
154
  # @param a [String] First version string
154
155
  # @param b [String] Second version string
155
- # @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
156
+ # @param scheme [String, nil] Package manager scheme or nil for generic comparison
156
157
  # @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
157
158
  #
158
159
  def self.compare_with_scheme(a, b, scheme)
@@ -165,22 +166,51 @@ module Vers
165
166
  # @param version_string [String] The version string to normalize
166
167
  # @return [String] The normalized version string
167
168
  #
168
- def self.normalize(version_string)
169
- Version.normalize(version_string)
169
+ def self.normalize(version_string, scheme = nil)
170
+ Version.normalize(version_string, scheme)
170
171
  end
171
172
 
172
173
  ##
173
174
  # Checks if a version string is valid
174
175
  #
175
176
  # @param version_string [String] The version string to validate
177
+ # @param scheme [String, nil] Package manager scheme or nil for generic validation
176
178
  # @return [Boolean] true if the version is valid
177
179
  #
178
- def self.valid?(version_string)
179
- Version.valid?(version_string)
180
+ def self.valid?(version_string, scheme = nil)
181
+ Version.valid?(version_string, scheme)
180
182
  end
181
183
 
182
- def self.clean(version_string)
183
- Version.clean(version_string)
184
+ ##
185
+ # Checks if a version is stable using scheme-specific rules
186
+ #
187
+ # @param version_string [String] The version string to classify
188
+ # @param scheme [String, nil] Package manager scheme or nil for generic classification
189
+ # @return [Boolean] true if the version is stable
190
+ #
191
+ def self.stable?(version_string, scheme = nil)
192
+ Version.stable?(version_string, scheme)
193
+ end
194
+
195
+ ##
196
+ # Checks if a version is a prerelease using scheme-specific rules
197
+ #
198
+ # @param version_string [String] The version string to classify
199
+ # @param scheme [String, nil] Package manager scheme or nil for generic classification
200
+ # @return [Boolean] true if the version is a prerelease
201
+ #
202
+ def self.prerelease?(version_string, scheme = nil)
203
+ Version.prerelease?(version_string, scheme)
204
+ end
205
+
206
+ # Returns a normalized version, or nil when the version is invalid.
207
+ #
208
+ # @param version_string [String] The version string to clean
209
+ # @param scheme [String, nil] Package manager scheme or nil for generic cleaning
210
+ # @return [String, nil] The normalized version or nil
211
+ #
212
+ def self.clean(version_string, scheme = nil)
213
+ Version.clean(version_string, scheme)
184
214
  end
185
215
 
186
216
  ##
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -29,12 +29,23 @@ files:
29
29
  - SECURITY.md
30
30
  - VERSION-RANGE-SPEC.rst
31
31
  - lib/vers.rb
32
+ - lib/vers/bazel_version.rb
33
+ - lib/vers/composer_version.rb
34
+ - lib/vers/conan_version.rb
32
35
  - lib/vers/constraint.rb
36
+ - lib/vers/distribution_version.rb
37
+ - lib/vers/gem_version.rb
33
38
  - lib/vers/interval.rb
34
39
  - lib/vers/maven_version.rb
35
40
  - lib/vers/nuget_version.rb
36
41
  - lib/vers/parser.rb
42
+ - lib/vers/pub_version.rb
43
+ - lib/vers/pypi_version.rb
44
+ - lib/vers/scheme.rb
45
+ - lib/vers/semver_version.rb
46
+ - lib/vers/special_version.rb
37
47
  - lib/vers/version.rb
48
+ - lib/vers/version_comparison.rb
38
49
  - lib/vers/version_range.rb
39
50
  - sig/vers.rbs
40
51
  - test-suite-data.json
@@ -44,7 +55,7 @@ licenses:
44
55
  metadata:
45
56
  allowed_push_host: https://rubygems.org
46
57
  homepage_uri: https://github.com/andrew/vers
47
- source_code_uri: https://github.com/andrew/vers
58
+ source_code_uri: https://github.com/andrew/vers/tree/v2.0.0
48
59
  changelog_uri: https://github.com/andrew/vers/blob/main/CHANGELOG.md
49
60
  rdoc_options: []
50
61
  require_paths:
@@ -53,14 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
64
  requirements:
54
65
  - - ">="
55
66
  - !ruby/object:Gem::Version
56
- version: 3.2.0
67
+ version: 3.3.0
57
68
  required_rubygems_version: !ruby/object:Gem::Requirement
58
69
  requirements:
59
70
  - - ">="
60
71
  - !ruby/object:Gem::Version
61
72
  version: '0'
62
73
  requirements: []
63
- rubygems_version: 4.0.3
74
+ rubygems_version: 4.0.16
64
75
  specification_version: 4
65
76
  summary: A Ruby gem for parsing, comparing and sorting versions according to the VERS
66
77
  spec.