vers 1.1.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b7aa0b15219b0734ba33605567d7f6283c51b06afeb66d65f6b6893ada38b95
4
- data.tar.gz: 79f6860358cba90b4d744af1af5c4198328f14a1bd555373018c3943b591c1ea
3
+ metadata.gz: 8ad669b5ca951e8a64ddd205d39e083b3a9836a67ca9ff25d99872a57658a560
4
+ data.tar.gz: d1aefb9f16a5a842ad05d4e427b538a565240cf3c21969542bf614199e8e1955
5
5
  SHA512:
6
- metadata.gz: 27d9b7fc130fc8a70f738114c5566d4cefbe092259c9a181d006322aecf6f2dc656474c99ee610b2ffc5fe58a9b84410c2b322813c213f63473d929e21286e4d
7
- data.tar.gz: 58035172c00bbbd2d8e8e9a96aa22bbae5eff4f0de8c8e2dbfe4c5340cc4267c0e28c8766705d527a0b387a788dd12841bad6a31ebb399c4f4a0663975f641c0
6
+ metadata.gz: 6b87591c17a303bf820c2667f25ac802569173737c8bd63d13c86bc5aedf6e606cc50d3906c08d526dd8c9e934559f0cdc7717e8e6c4cfbd8ae5473ebc53bb9c
7
+ data.tar.gz: 9a6cafa4929f8a71b699d6ce1171e574bb6c99aca17b89850e7bf232d1269727d00f6a15f1b9a870f8319913e77b0ec865fc5633866161cb989425c9eefc76ce
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "test/vers-spec"]
2
+ path = test/vers-spec
3
+ url = https://github.com/package-url/vers-spec.git
data/lib/vers/parser.rb CHANGED
@@ -109,16 +109,26 @@ module Vers
109
109
  # @return [String] The vers URI string
110
110
  #
111
111
  def to_vers_string(version_range, scheme)
112
- return "*" if version_range.unbounded?
112
+ return "vers:#{scheme}/*" if version_range.unbounded?
113
113
  return "vers:#{scheme}/" if version_range.empty?
114
114
 
115
115
  intervals = version_range.raw_constraints || version_range.intervals
116
116
  constraints = []
117
117
 
118
+ # Detect != pattern: two intervals (-∞,V) ∪ (V,+∞)
119
+ if intervals.length == 2
120
+ a, b = intervals
121
+ if a.min.nil? && !a.max_inclusive && b.max.nil? && !b.min_inclusive && a.max == b.min
122
+ constraints << "!=#{a.max}"
123
+ constraints.sort_by! { |c| sort_key_for_constraint(c) }
124
+ return "vers:#{scheme}/#{constraints.join('|')}"
125
+ end
126
+ end
127
+
118
128
  intervals.each do |interval|
119
129
  if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
120
130
  # Exact version
121
- constraints << "=#{interval.min}"
131
+ constraints << interval.min.to_s
122
132
  else
123
133
  # Range constraints
124
134
  if interval.min
@@ -134,7 +144,6 @@ module Vers
134
144
  end
135
145
 
136
146
  constraints.sort_by! { |c| sort_key_for_constraint(c) }
137
- constraints.uniq!
138
147
 
139
148
  "vers:#{scheme}/#{constraints.join('|')}"
140
149
  end
@@ -144,7 +153,7 @@ module Vers
144
153
  def sort_key_for_constraint(constraint)
145
154
  version = constraint.sub(/\A[><=!]+/, '')
146
155
  v = Version.cached_new(version)
147
- [v.major || 0, v.minor || 0, v.patch || 0, constraint]
156
+ [v, constraint]
148
157
  end
149
158
 
150
159
  def parse_constraints(constraints_string, scheme)
@@ -164,8 +173,14 @@ module Vers
164
173
  end
165
174
  end
166
175
 
167
- # Start with the union of all positive constraints
168
- range = VersionRange.new(intervals, scheme: interval_scheme)
176
+ # Start with the union of all positive constraints, or unbounded if only exclusions
177
+ range = if intervals.any?
178
+ VersionRange.new(intervals, scheme: interval_scheme)
179
+ elsif exclusions.any?
180
+ VersionRange.unbounded
181
+ else
182
+ VersionRange.new([], scheme: interval_scheme)
183
+ end
169
184
 
170
185
  # Apply exclusions
171
186
  exclusions.each do |version|
@@ -185,7 +200,7 @@ module Vers
185
200
  # Handle || (OR) operator
186
201
  if range_string.include?('||')
187
202
  or_parts = range_string.split('||').map(&:strip)
188
- ranges = or_parts.map { |part| parse_npm_single_range(part) }
203
+ ranges = or_parts.map { |part| parse_npm_range(part) }
189
204
  return ranges.reduce { |acc, range| acc.union(range) }
190
205
  end
191
206
 
@@ -206,7 +221,13 @@ module Vers
206
221
  end
207
222
  end
208
223
  ranges = merged.map { |part| parse_npm_single_range(part) }
209
- ranges.reduce { |acc, range| acc.intersect(range) }
224
+ # If all parts are bare versions (no operators), treat as union
225
+ all_exact = merged.all? { |part| part.match?(/\A\d/) }
226
+ if all_exact
227
+ ranges.reduce { |acc, range| acc.union(range) }
228
+ else
229
+ ranges.reduce { |acc, range| acc.intersect(range) }
230
+ end
210
231
  end
211
232
 
212
233
  def parse_npm_single_range(range_string)
@@ -256,8 +277,25 @@ module Vers
256
277
  # Invalid patterns that should raise errors
257
278
  raise ArgumentError, "Invalid NPM range format: #{range_string}"
258
279
  else
280
+ # Check for operator + x-range (e.g. ">=2.2.x", ">=1.x")
281
+ if range_string.match(/\A[><=]+(\d+)\.[xX*]\z/)
282
+ major = $1.to_i
283
+ return VersionRange.new([
284
+ Interval.new(min: "#{major}.0.0", max: "#{major + 1}.0.0", min_inclusive: true, max_inclusive: false)
285
+ ])
286
+ end
287
+ if range_string.match(/\A[><=]+(\d+)\.(\d+)\.[xX*]\z/)
288
+ major = $1.to_i
289
+ minor = $2.to_i
290
+ return VersionRange.new([
291
+ Interval.new(min: "#{major}.#{minor}.0", max: "#{major}.#{minor + 1}.0", min_inclusive: true, max_inclusive: false)
292
+ ])
293
+ end
259
294
  # Standard constraint
260
295
  constraint = Constraint.parse(range_string)
296
+ # Normalize version to semver (npm always uses 3 segments)
297
+ normalized_version = Version.cached_new(constraint.version).to_s
298
+ constraint = Constraint.new(constraint.operator, normalized_version)
261
299
  if constraint.exclusion?
262
300
  VersionRange.unbounded.exclude(constraint.version)
263
301
  else
@@ -289,9 +327,27 @@ module Vers
289
327
 
290
328
  def parse_tilde_range(version)
291
329
  v = Version.cached_new(version)
292
- upper_version = if v.minor
330
+
331
+ if v.prerelease
332
+ # ~0.8.0-pre := >=0.8.0-pre <0.8.0 OR >=0.8.0 <0.8.1
333
+ # Prereleases only match their own major.minor.patch
334
+ base = "#{v.major}.#{v.minor || 0}.#{v.patch || 0}"
335
+ next_patch = "#{v.major}.#{v.minor || 0}.#{(v.patch || 0) + 1}"
336
+ pre_range = VersionRange.new([
337
+ Interval.new(min: version, max: base, min_inclusive: true, max_inclusive: false)
338
+ ])
339
+ release_range = VersionRange.new([
340
+ Interval.new(min: base, max: next_patch, min_inclusive: true, max_inclusive: false)
341
+ ])
342
+ return pre_range.union(release_range)
343
+ end
344
+
345
+ upper_version = if v.patch
293
346
  # ~1.2.3 := >=1.2.3 <1.3.0
294
347
  "#{v.major}.#{v.minor + 1}.0"
348
+ elsif v.minor
349
+ # ~1.2 := >=1.2.0 <1.3.0
350
+ "#{v.major}.#{v.minor + 1}.0"
295
351
  else
296
352
  # ~1 := >=1.0.0 <2.0.0
297
353
  "#{v.major + 1}.0.0"
@@ -318,14 +374,14 @@ module Vers
318
374
  def parse_pessimistic_range(version)
319
375
  v = Version.cached_new(version)
320
376
  upper_version = if v.patch
321
- # ~> 1.2.3 := >= 1.2.3, < 1.3.0
322
- "#{v.major}.#{v.minor + 1}.0"
377
+ # ~> 1.2.3 := >= 1.2.3, < 1.3
378
+ "#{v.major}.#{v.minor + 1}"
323
379
  elsif v.minor
324
- # ~> 1.2 := >= 1.2.0, < 2.0.0
325
- "#{v.major + 1}.0.0"
380
+ # ~> 1.2 := >= 1.2.0, < 2
381
+ "#{v.major + 1}"
326
382
  else
327
- # ~> 1 := >= 1.0.0, < 2.0.0
328
- "#{v.major + 1}.0.0"
383
+ # ~> 1 := >= 1.0.0, < 2
384
+ "#{v.major + 1}"
329
385
  end
330
386
 
331
387
  VersionRange.new([
@@ -550,9 +606,9 @@ module Vers
550
606
  end
551
607
 
552
608
  def parse_hex_single_range(range_string)
553
- # Handle "and" conjunction
554
- if range_string.include?(" and ")
555
- and_parts = range_string.split(" and ").map(&:strip)
609
+ # Handle "and" conjunction and comma-separated AND constraints
610
+ if range_string.include?(" and ") || range_string.include?(",")
611
+ and_parts = range_string.split(/\s+and\s+|,/).map(&:strip).reject(&:empty?)
556
612
  ranges = and_parts.map { |part| parse_hex_constraint(part) }
557
613
  return ranges.reduce { |acc, range| acc.intersect(range) }
558
614
  end
data/lib/vers/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vers
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
 
6
6
  ##
7
7
  # Handles version comparison and normalization across different package ecosystems.
@@ -104,10 +104,12 @@ module Vers
104
104
  # @return [Boolean] true if the version is valid
105
105
  #
106
106
  def self.valid?(version_string)
107
- cached_new(version_string)
108
- true
109
- rescue ArgumentError
110
- false
107
+ version_string.to_s.match?(/\Av?\d+\.\d+\.\d+/)
108
+ end
109
+
110
+ def self.clean(version_string)
111
+ return nil unless valid?(version_string)
112
+ version_string.to_s.sub(/\Av/, '')
111
113
  end
112
114
 
113
115
  ##
data/lib/vers.rb CHANGED
@@ -119,13 +119,15 @@ module Vers
119
119
  # Vers.satisfies?("1.5.0", "^1.2.3", "npm") # => true
120
120
  #
121
121
  def self.satisfies?(version, constraint, scheme = nil)
122
- range = if scheme
123
- parse_native(constraint, scheme)
124
- else
125
- parse(constraint)
126
- end
127
-
128
- range.contains?(version)
122
+ sub_ranges = constraint.split('||').map(&:strip).reject(&:empty?)
123
+ sub_ranges.any? do |sub_range|
124
+ range = if scheme
125
+ parse_native(sub_range, scheme)
126
+ else
127
+ parse(sub_range)
128
+ end
129
+ range.contains?(version)
130
+ end
129
131
  end
130
132
 
131
133
  ##
@@ -177,6 +179,10 @@ module Vers
177
179
  Version.valid?(version_string)
178
180
  end
179
181
 
182
+ def self.clean(version_string)
183
+ Version.clean(version_string)
184
+ end
185
+
180
186
  ##
181
187
  # Creates an exact version range
182
188
  #
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -18,6 +18,7 @@ executables: []
18
18
  extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
+ - ".gitmodules"
21
22
  - ".ruby-version"
22
23
  - CHANGELOG.md
23
24
  - CODE_OF_CONDUCT.md