vers 1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +87 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +194 -0
- data/README.md +257 -0
- data/Rakefile +248 -0
- data/SECURITY.md +164 -0
- data/VERSION-RANGE-SPEC.rst +1009 -0
- data/lib/vers/constraint.rb +158 -0
- data/lib/vers/interval.rb +229 -0
- data/lib/vers/parser.rb +447 -0
- data/lib/vers/version.rb +338 -0
- data/lib/vers/version_range.rb +173 -0
- data/lib/vers.rb +215 -0
- data/sig/vers.rbs +4 -0
- data/test-suite-data.json +335 -0
- metadata +61 -0
data/lib/vers/version.rb
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vers
|
4
|
+
VERSION = "1.0.0"
|
5
|
+
|
6
|
+
##
|
7
|
+
# Handles version comparison and normalization across different package ecosystems.
|
8
|
+
#
|
9
|
+
# This class provides version comparison functionality that can handle different
|
10
|
+
# versioning schemes used by various package managers (npm, gem, pypi, etc.).
|
11
|
+
#
|
12
|
+
# == Examples
|
13
|
+
#
|
14
|
+
# Vers::Version.compare("1.2.3", "1.2.4") # => -1
|
15
|
+
# Vers::Version.compare("2.0.0", "1.9.9") # => 1
|
16
|
+
# Vers::Version.compare("1.0.0", "1.0.0") # => 0
|
17
|
+
#
|
18
|
+
class Version
|
19
|
+
# Regex for parsing semantic version components including build metadata
|
20
|
+
SEMANTIC_VERSION_REGEX = /\A(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([^+]+))?(?:\+(.+))?\z/
|
21
|
+
|
22
|
+
attr_reader :major, :minor, :patch, :prerelease, :build
|
23
|
+
|
24
|
+
##
|
25
|
+
# Creates a new Version object
|
26
|
+
#
|
27
|
+
# @param version_string [String] The version string to parse
|
28
|
+
#
|
29
|
+
def initialize(version_string)
|
30
|
+
@original = version_string.to_s
|
31
|
+
parse_version
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Compares two version strings
|
36
|
+
#
|
37
|
+
# @param a [String] First version string
|
38
|
+
# @param b [String] Second version string
|
39
|
+
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
40
|
+
#
|
41
|
+
def self.compare(a, b)
|
42
|
+
return 0 if a == b
|
43
|
+
return -1 if a.nil?
|
44
|
+
return 1 if b.nil?
|
45
|
+
|
46
|
+
version_a = new(a)
|
47
|
+
version_b = new(b)
|
48
|
+
|
49
|
+
version_a <=> version_b
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Normalizes a version string to a consistent format
|
54
|
+
#
|
55
|
+
# @param version_string [String] The version string to normalize
|
56
|
+
# @return [String] The normalized version string
|
57
|
+
#
|
58
|
+
def self.normalize(version_string)
|
59
|
+
new(version_string).to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Checks if a version string is valid
|
64
|
+
#
|
65
|
+
# @param version_string [String] The version string to validate
|
66
|
+
# @return [Boolean] true if the version is valid
|
67
|
+
#
|
68
|
+
def self.valid?(version_string)
|
69
|
+
new(version_string)
|
70
|
+
true
|
71
|
+
rescue ArgumentError
|
72
|
+
false
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Version comparison operator
|
77
|
+
#
|
78
|
+
# @param other [Version] The other version to compare to
|
79
|
+
# @return [Integer] -1, 0, or 1
|
80
|
+
#
|
81
|
+
def <=>(other)
|
82
|
+
return 0 if @original == other.to_s
|
83
|
+
|
84
|
+
# Compare major.minor.patch numerically
|
85
|
+
major_cmp = (major || 0) <=> (other.major || 0)
|
86
|
+
return major_cmp unless major_cmp == 0
|
87
|
+
|
88
|
+
minor_cmp = (minor || 0) <=> (other.minor || 0)
|
89
|
+
return minor_cmp unless minor_cmp == 0
|
90
|
+
|
91
|
+
patch_cmp = (patch || 0) <=> (other.patch || 0)
|
92
|
+
return patch_cmp unless patch_cmp == 0
|
93
|
+
|
94
|
+
# Handle prerelease comparison
|
95
|
+
return 1 if prerelease.nil? && !other.prerelease.nil?
|
96
|
+
return -1 if !prerelease.nil? && other.prerelease.nil?
|
97
|
+
return 0 if prerelease.nil? && other.prerelease.nil?
|
98
|
+
|
99
|
+
compare_prerelease(prerelease, other.prerelease)
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# String representation of the version
|
104
|
+
#
|
105
|
+
# @return [String] The normalized version string
|
106
|
+
#
|
107
|
+
def to_s
|
108
|
+
version = "#{major || 0}"
|
109
|
+
version += ".#{minor || 0}"
|
110
|
+
version += ".#{patch || 0}"
|
111
|
+
version += "-#{prerelease}" if prerelease
|
112
|
+
version
|
113
|
+
end
|
114
|
+
|
115
|
+
def ==(other)
|
116
|
+
other.is_a?(Version) && self <=> other == 0
|
117
|
+
end
|
118
|
+
|
119
|
+
def <(other)
|
120
|
+
(self <=> other) < 0
|
121
|
+
end
|
122
|
+
|
123
|
+
def <=(other)
|
124
|
+
(self <=> other) <= 0
|
125
|
+
end
|
126
|
+
|
127
|
+
def >(other)
|
128
|
+
(self <=> other) > 0
|
129
|
+
end
|
130
|
+
|
131
|
+
def >=(other)
|
132
|
+
(self <=> other) >= 0
|
133
|
+
end
|
134
|
+
|
135
|
+
def hash
|
136
|
+
[@original].hash
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Increments the specified component of the version
|
141
|
+
#
|
142
|
+
# @param component [Symbol] The component to increment (:major, :minor, :patch)
|
143
|
+
# @return [Version] A new Version object with the incremented component
|
144
|
+
#
|
145
|
+
# == Examples
|
146
|
+
#
|
147
|
+
# version = Vers::Version.new("1.2.3")
|
148
|
+
# version.increment(:major) # => #<Vers::Version "2.0.0">
|
149
|
+
# version.increment(:minor) # => #<Vers::Version "1.3.0">
|
150
|
+
# version.increment(:patch) # => #<Vers::Version "1.2.4">
|
151
|
+
#
|
152
|
+
def increment(component)
|
153
|
+
case component
|
154
|
+
when :major
|
155
|
+
self.class.new("#{major + 1}.0.0")
|
156
|
+
when :minor
|
157
|
+
self.class.new("#{major}.#{(minor || 0) + 1}.0")
|
158
|
+
when :patch
|
159
|
+
self.class.new("#{major}.#{minor || 0}.#{(patch || 0) + 1}")
|
160
|
+
else
|
161
|
+
raise ArgumentError, "Invalid component: #{component}. Must be :major, :minor, or :patch"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Increments the major version component
|
167
|
+
#
|
168
|
+
# @return [Version] A new Version object with incremented major version
|
169
|
+
#
|
170
|
+
def increment_major
|
171
|
+
increment(:major)
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Increments the minor version component
|
176
|
+
#
|
177
|
+
# @return [Version] A new Version object with incremented minor version
|
178
|
+
#
|
179
|
+
def increment_minor
|
180
|
+
increment(:minor)
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Increments the patch version component
|
185
|
+
#
|
186
|
+
# @return [Version] A new Version object with incremented patch version
|
187
|
+
#
|
188
|
+
def increment_patch
|
189
|
+
increment(:patch)
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Checks if this version satisfies a constraint using pessimistic operator logic
|
194
|
+
#
|
195
|
+
# @param constraint [String] The constraint string (e.g., "~> 1.2")
|
196
|
+
# @return [Boolean] true if this version satisfies the constraint
|
197
|
+
#
|
198
|
+
# == Examples
|
199
|
+
#
|
200
|
+
# version = Vers::Version.new("1.2.5")
|
201
|
+
# version.satisfies?("~> 1.2") # => true (>= 1.2.0, < 1.3.0)
|
202
|
+
# version.satisfies?("~> 1.2.3") # => true (>= 1.2.3, < 1.3.0)
|
203
|
+
# version.satisfies?("~> 1.3") # => false
|
204
|
+
#
|
205
|
+
def satisfies?(constraint)
|
206
|
+
if constraint.start_with?("~>")
|
207
|
+
# Pessimistic constraint
|
208
|
+
base_version = constraint.sub(/^~>\s*/, "").strip
|
209
|
+
base = self.class.new(base_version)
|
210
|
+
|
211
|
+
# Must be >= base version
|
212
|
+
return false if self < base
|
213
|
+
|
214
|
+
# Must be < next significant version
|
215
|
+
if base.patch && base.patch > 0
|
216
|
+
# ~> 1.2.3 means >= 1.2.3, < 1.3.0
|
217
|
+
upper_bound = self.class.new("#{base.major}.#{(base.minor || 0) + 1}.0")
|
218
|
+
elsif base.minor
|
219
|
+
# ~> 1.2 means >= 1.2.0, < 1.3.0
|
220
|
+
upper_bound = self.class.new("#{base.major}.#{(base.minor || 0) + 1}.0")
|
221
|
+
else
|
222
|
+
# ~> 1 means >= 1.0.0, < 2.0.0
|
223
|
+
upper_bound = self.class.new("#{base.major + 1}.0.0")
|
224
|
+
end
|
225
|
+
|
226
|
+
self < upper_bound
|
227
|
+
else
|
228
|
+
# For other constraints, delegate to constraint parsing
|
229
|
+
# This would require the Constraint class, so for now return true
|
230
|
+
true
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# Checks if this is a stable release (no prerelease components)
|
236
|
+
#
|
237
|
+
# @return [Boolean] true if this is a stable release
|
238
|
+
#
|
239
|
+
def stable?
|
240
|
+
prerelease.nil?
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Checks if this is a prerelease version
|
245
|
+
#
|
246
|
+
# @return [Boolean] true if this is a prerelease version
|
247
|
+
#
|
248
|
+
def prerelease?
|
249
|
+
!prerelease.nil?
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# Gets the semantic version components as a hash
|
254
|
+
#
|
255
|
+
# @return [Hash] Hash with :major, :minor, :patch, :prerelease, :build keys
|
256
|
+
#
|
257
|
+
def to_h
|
258
|
+
{
|
259
|
+
major: major,
|
260
|
+
minor: minor,
|
261
|
+
patch: patch,
|
262
|
+
prerelease: prerelease,
|
263
|
+
build: build
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
##
|
268
|
+
# Creates a new Version with the same major.minor but patch set to 0
|
269
|
+
#
|
270
|
+
# @return [Version] A new Version object with patch reset to 0
|
271
|
+
#
|
272
|
+
def base
|
273
|
+
self.class.new("#{major}.#{minor || 0}.0")
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
def parse_version
|
279
|
+
# Handle simple numeric versions
|
280
|
+
if @original.match(/^\d+$/)
|
281
|
+
@major = @original.to_i
|
282
|
+
return
|
283
|
+
end
|
284
|
+
|
285
|
+
# Try semantic version parsing first
|
286
|
+
if match = @original.match(SEMANTIC_VERSION_REGEX)
|
287
|
+
@major = match[1]&.to_i
|
288
|
+
@minor = match[2]&.to_i
|
289
|
+
@patch = match[3]&.to_i
|
290
|
+
@prerelease = match[4]
|
291
|
+
@build = match[5]
|
292
|
+
return
|
293
|
+
end
|
294
|
+
|
295
|
+
# Fall back to splitting on dots/dashes
|
296
|
+
parts = @original.split(/[.-]/)
|
297
|
+
|
298
|
+
if parts.empty?
|
299
|
+
raise ArgumentError, "Invalid version format: #{@original}"
|
300
|
+
end
|
301
|
+
|
302
|
+
@major = parts[0]&.to_i
|
303
|
+
@minor = parts[1]&.to_i if parts[1]
|
304
|
+
@patch = parts[2]&.to_i if parts[2]
|
305
|
+
|
306
|
+
# Everything after patch is considered prerelease
|
307
|
+
if parts.length > 3
|
308
|
+
@prerelease = parts[3..-1].join('.')
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def compare_prerelease(pre_a, pre_b)
|
313
|
+
parts_a = pre_a.split('.')
|
314
|
+
parts_b = pre_b.split('.')
|
315
|
+
|
316
|
+
max_length = [parts_a.length, parts_b.length].max
|
317
|
+
|
318
|
+
0.upto(max_length - 1) do |i|
|
319
|
+
part_a = parts_a[i]
|
320
|
+
part_b = parts_b[i]
|
321
|
+
|
322
|
+
return -1 if part_a.nil?
|
323
|
+
return 1 if part_b.nil?
|
324
|
+
|
325
|
+
# Try numeric comparison first
|
326
|
+
if part_a.match(/^\d+$/) && part_b.match(/^\d+$/)
|
327
|
+
numeric_cmp = part_a.to_i <=> part_b.to_i
|
328
|
+
return numeric_cmp unless numeric_cmp == 0
|
329
|
+
else
|
330
|
+
string_cmp = part_a <=> part_b
|
331
|
+
return string_cmp unless string_cmp == 0
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
0
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'interval'
|
4
|
+
require_relative 'version'
|
5
|
+
|
6
|
+
module Vers
|
7
|
+
class VersionRange
|
8
|
+
attr_reader :intervals
|
9
|
+
|
10
|
+
def initialize(intervals = [])
|
11
|
+
@intervals = intervals.compact.reject(&:empty?).sort_by { |i| [i.min || '', i.max || ''] }
|
12
|
+
merge_overlapping_intervals!
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.empty
|
16
|
+
new([])
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.unbounded
|
20
|
+
new([Interval.unbounded])
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.exact(version)
|
24
|
+
new([Interval.exact(version)])
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.greater_than(version, inclusive: false)
|
28
|
+
new([Interval.greater_than(version, inclusive: inclusive)])
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.less_than(version, inclusive: false)
|
32
|
+
new([Interval.less_than(version, inclusive: inclusive)])
|
33
|
+
end
|
34
|
+
|
35
|
+
def empty?
|
36
|
+
intervals.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def unbounded?
|
40
|
+
intervals.length == 1 && intervals.first.unbounded?
|
41
|
+
end
|
42
|
+
|
43
|
+
def contains?(version)
|
44
|
+
intervals.any? { |interval| interval.contains?(version) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def intersect(other)
|
48
|
+
result_intervals = []
|
49
|
+
|
50
|
+
intervals.each do |interval1|
|
51
|
+
other.intervals.each do |interval2|
|
52
|
+
intersection = interval1.intersect(interval2)
|
53
|
+
result_intervals << intersection unless intersection.empty?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
self.class.new(result_intervals)
|
58
|
+
end
|
59
|
+
|
60
|
+
def union(other)
|
61
|
+
self.class.new(intervals + other.intervals)
|
62
|
+
end
|
63
|
+
|
64
|
+
def complement
|
65
|
+
return self.class.unbounded if empty?
|
66
|
+
return self.class.empty if unbounded?
|
67
|
+
|
68
|
+
result_intervals = []
|
69
|
+
|
70
|
+
sorted_intervals = intervals.sort_by { |i| i.min || '' }
|
71
|
+
|
72
|
+
first_interval = sorted_intervals.first
|
73
|
+
if first_interval.min
|
74
|
+
result_intervals << Interval.new(
|
75
|
+
max: first_interval.min,
|
76
|
+
max_inclusive: !first_interval.min_inclusive
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
sorted_intervals.each_cons(2) do |curr, next_interval|
|
81
|
+
if curr.max && next_interval.min
|
82
|
+
comparison = version_compare(curr.max, next_interval.min)
|
83
|
+
if comparison < 0 || (comparison == 0 && (!curr.max_inclusive || !next_interval.min_inclusive))
|
84
|
+
result_intervals << Interval.new(
|
85
|
+
min: curr.max,
|
86
|
+
max: next_interval.min,
|
87
|
+
min_inclusive: !curr.max_inclusive,
|
88
|
+
max_inclusive: !next_interval.min_inclusive
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
last_interval = sorted_intervals.last
|
95
|
+
if last_interval.max
|
96
|
+
result_intervals << Interval.new(
|
97
|
+
min: last_interval.max,
|
98
|
+
min_inclusive: !last_interval.max_inclusive
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
self.class.new(result_intervals)
|
103
|
+
end
|
104
|
+
|
105
|
+
def exclude(version)
|
106
|
+
return self if !contains?(version)
|
107
|
+
|
108
|
+
result_intervals = []
|
109
|
+
|
110
|
+
intervals.each do |interval|
|
111
|
+
if interval.contains?(version)
|
112
|
+
if interval.min && version_compare(interval.min, version) < 0
|
113
|
+
result_intervals << Interval.new(
|
114
|
+
min: interval.min,
|
115
|
+
max: version,
|
116
|
+
min_inclusive: interval.min_inclusive,
|
117
|
+
max_inclusive: false
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
if interval.max && version_compare(version, interval.max) < 0
|
122
|
+
result_intervals << Interval.new(
|
123
|
+
min: version,
|
124
|
+
max: interval.max,
|
125
|
+
min_inclusive: false,
|
126
|
+
max_inclusive: interval.max_inclusive
|
127
|
+
)
|
128
|
+
end
|
129
|
+
else
|
130
|
+
result_intervals << interval
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
self.class.new(result_intervals)
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
return "∅" if empty?
|
139
|
+
return intervals.map(&:to_s).join(" ∪ ")
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def merge_overlapping_intervals!
|
145
|
+
return if intervals.length <= 1
|
146
|
+
|
147
|
+
merged = []
|
148
|
+
current = intervals.first
|
149
|
+
|
150
|
+
intervals[1..-1].each do |interval|
|
151
|
+
union_result = current.union(interval)
|
152
|
+
if union_result
|
153
|
+
current = union_result
|
154
|
+
else
|
155
|
+
merged << current
|
156
|
+
current = interval
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
merged << current
|
161
|
+
@intervals = merged
|
162
|
+
end
|
163
|
+
|
164
|
+
def version_compare(a, b)
|
165
|
+
return 0 if a == b
|
166
|
+
return -1 if a.nil?
|
167
|
+
return 1 if b.nil?
|
168
|
+
|
169
|
+
# Use the Version class for comparison
|
170
|
+
Version.compare(a, b)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/vers.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "vers/version"
|
4
|
+
require_relative "vers/interval"
|
5
|
+
require_relative "vers/version_range"
|
6
|
+
require_relative "vers/constraint"
|
7
|
+
require_relative "vers/parser"
|
8
|
+
|
9
|
+
##
|
10
|
+
# Vers - A Ruby gem for parsing, comparing and sorting versions according to the VERS spec
|
11
|
+
#
|
12
|
+
# This gem provides tools for working with version ranges across different package managers,
|
13
|
+
# using a mathematical interval model internally and supporting the vers specification from
|
14
|
+
# the Package URL (PURL) project.
|
15
|
+
#
|
16
|
+
# == Features
|
17
|
+
#
|
18
|
+
# * Parse version ranges from multiple package ecosystems (npm, gem, pypi, maven, etc.)
|
19
|
+
# * Convert between native version range syntax and universal vers URI format
|
20
|
+
# * Mathematical interval-based operations (union, intersection, complement)
|
21
|
+
# * Version comparison and containment checking
|
22
|
+
# * Extensible architecture for adding new package manager support
|
23
|
+
#
|
24
|
+
# == Quick Start
|
25
|
+
#
|
26
|
+
# require 'vers'
|
27
|
+
#
|
28
|
+
# # Parse a vers URI
|
29
|
+
# range = Vers.parse("vers:npm/>=1.2.3|<2.0.0")
|
30
|
+
# range.contains?("1.5.0") # => true
|
31
|
+
# range.contains?("2.1.0") # => false
|
32
|
+
#
|
33
|
+
# # Parse native package manager syntax
|
34
|
+
# npm_range = Vers.parse_native("^1.2.3", "npm")
|
35
|
+
# gem_range = Vers.parse_native("~> 1.0", "gem")
|
36
|
+
#
|
37
|
+
# # Check version containment
|
38
|
+
# Vers.satisfies?("1.5.0", ">=1.0.0,<2.0.0") # => true
|
39
|
+
#
|
40
|
+
# # Compare versions
|
41
|
+
# Vers.compare("1.2.3", "1.2.4") # => -1
|
42
|
+
#
|
43
|
+
# == Mathematical Model
|
44
|
+
#
|
45
|
+
# Internally, all version ranges are represented as mathematical intervals,
|
46
|
+
# similar to those used in mathematics (e.g., [1.0.0, 2.0.0) represents
|
47
|
+
# versions from 1.0.0 inclusive to 2.0.0 exclusive).
|
48
|
+
#
|
49
|
+
# This allows for precise set operations like union, intersection, and
|
50
|
+
# complement, regardless of the original package manager syntax.
|
51
|
+
#
|
52
|
+
# @see https://github.com/package-url/purl-spec/blob/main/VERSION-RANGE-SPEC.rst
|
53
|
+
# @author Andrew Nesbitt
|
54
|
+
#
|
55
|
+
module Vers
|
56
|
+
class Error < StandardError; end
|
57
|
+
|
58
|
+
# Default parser instance for convenience methods
|
59
|
+
@@parser = Parser.new
|
60
|
+
|
61
|
+
##
|
62
|
+
# Parses a vers URI string into a VersionRange
|
63
|
+
#
|
64
|
+
# @param vers_string [String] The vers URI string (e.g., "vers:npm/>=1.2.3|<2.0.0")
|
65
|
+
# @return [VersionRange] The parsed version range
|
66
|
+
# @raise [ArgumentError] if the vers string is invalid
|
67
|
+
#
|
68
|
+
# == Examples
|
69
|
+
#
|
70
|
+
# Vers.parse("vers:npm/>=1.2.3|<2.0.0")
|
71
|
+
# Vers.parse("vers:gem/~>1.0")
|
72
|
+
# Vers.parse("*") # unbounded range
|
73
|
+
#
|
74
|
+
def self.parse(vers_string)
|
75
|
+
@@parser.parse(vers_string)
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Parses a native package manager version range into a VersionRange
|
80
|
+
#
|
81
|
+
# @param range_string [String] The native version range string
|
82
|
+
# @param scheme [String] The package manager scheme (npm, gem, pypi, etc.)
|
83
|
+
# @return [VersionRange] The parsed version range
|
84
|
+
#
|
85
|
+
# == Examples
|
86
|
+
#
|
87
|
+
# Vers.parse_native("^1.2.3", "npm") # npm caret range
|
88
|
+
# Vers.parse_native("~> 1.0", "gem") # gem pessimistic operator
|
89
|
+
# Vers.parse_native(">=1.0,<2.0", "pypi") # python constraints
|
90
|
+
#
|
91
|
+
def self.parse_native(range_string, scheme)
|
92
|
+
@@parser.parse_native(range_string, scheme)
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Converts a VersionRange to a vers URI string
|
97
|
+
#
|
98
|
+
# @param version_range [VersionRange] The version range to convert
|
99
|
+
# @param scheme [String] The package manager scheme
|
100
|
+
# @return [String] The vers URI string
|
101
|
+
#
|
102
|
+
def self.to_vers_string(version_range, scheme)
|
103
|
+
@@parser.to_vers_string(version_range, scheme)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Checks if a version satisfies a version range constraint
|
108
|
+
#
|
109
|
+
# @param version [String] The version to check
|
110
|
+
# @param constraint [String] The version constraint (vers URI or native format)
|
111
|
+
# @param scheme [String, nil] The package manager scheme (if not using vers URI)
|
112
|
+
# @return [Boolean] true if the version satisfies the constraint
|
113
|
+
#
|
114
|
+
# == Examples
|
115
|
+
#
|
116
|
+
# Vers.satisfies?("1.5.0", "vers:npm/>=1.0.0|<2.0.0") # => true
|
117
|
+
# Vers.satisfies?("1.5.0", "^1.2.3", "npm") # => true
|
118
|
+
#
|
119
|
+
def self.satisfies?(version, constraint, scheme = nil)
|
120
|
+
range = if scheme
|
121
|
+
parse_native(constraint, scheme)
|
122
|
+
else
|
123
|
+
parse(constraint)
|
124
|
+
end
|
125
|
+
|
126
|
+
range.contains?(version)
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Compares two version strings
|
131
|
+
#
|
132
|
+
# @param a [String] First version string
|
133
|
+
# @param b [String] Second version string
|
134
|
+
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
135
|
+
#
|
136
|
+
# == Examples
|
137
|
+
#
|
138
|
+
# Vers.compare("1.2.3", "1.2.4") # => -1
|
139
|
+
# Vers.compare("2.0.0", "1.9.9") # => 1
|
140
|
+
# Vers.compare("1.0.0", "1.0.0") # => 0
|
141
|
+
#
|
142
|
+
def self.compare(a, b)
|
143
|
+
Version.compare(a, b)
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Normalizes a version string to a consistent format
|
148
|
+
#
|
149
|
+
# @param version_string [String] The version string to normalize
|
150
|
+
# @return [String] The normalized version string
|
151
|
+
#
|
152
|
+
def self.normalize(version_string)
|
153
|
+
Version.normalize(version_string)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Checks if a version string is valid
|
158
|
+
#
|
159
|
+
# @param version_string [String] The version string to validate
|
160
|
+
# @return [Boolean] true if the version is valid
|
161
|
+
#
|
162
|
+
def self.valid?(version_string)
|
163
|
+
Version.valid?(version_string)
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Creates an exact version range
|
168
|
+
#
|
169
|
+
# @param version [String] The exact version
|
170
|
+
# @return [VersionRange] A range containing only the specified version
|
171
|
+
#
|
172
|
+
def self.exact(version)
|
173
|
+
VersionRange.exact(version)
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Creates a greater-than version range
|
178
|
+
#
|
179
|
+
# @param version [String] The minimum version
|
180
|
+
# @param inclusive [Boolean] Whether to include the minimum version
|
181
|
+
# @return [VersionRange] A range for versions greater than (or equal to) the specified version
|
182
|
+
#
|
183
|
+
def self.greater_than(version, inclusive: false)
|
184
|
+
VersionRange.greater_than(version, inclusive: inclusive)
|
185
|
+
end
|
186
|
+
|
187
|
+
##
|
188
|
+
# Creates a less-than version range
|
189
|
+
#
|
190
|
+
# @param version [String] The maximum version
|
191
|
+
# @param inclusive [Boolean] Whether to include the maximum version
|
192
|
+
# @return [VersionRange] A range for versions less than (or equal to) the specified version
|
193
|
+
#
|
194
|
+
def self.less_than(version, inclusive: false)
|
195
|
+
VersionRange.less_than(version, inclusive: inclusive)
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Creates an unbounded version range (matches all versions)
|
200
|
+
#
|
201
|
+
# @return [VersionRange] An unbounded range
|
202
|
+
#
|
203
|
+
def self.unbounded
|
204
|
+
VersionRange.unbounded
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Creates an empty version range (matches no versions)
|
209
|
+
#
|
210
|
+
# @return [VersionRange] An empty range
|
211
|
+
#
|
212
|
+
def self.empty
|
213
|
+
VersionRange.empty
|
214
|
+
end
|
215
|
+
end
|