vers 1.0.3 → 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 +4 -4
- data/.gitmodules +3 -0
- data/CHANGELOG.md +11 -1
- data/lib/vers/constraint.rb +6 -6
- data/lib/vers/interval.rb +26 -18
- data/lib/vers/maven_version.rb +218 -0
- data/lib/vers/nuget_version.rb +79 -0
- data/lib/vers/parser.rb +259 -49
- data/lib/vers/version.rb +30 -6
- data/lib/vers/version_range.rb +92 -42
- data/lib/vers.rb +27 -7
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ad669b5ca951e8a64ddd205d39e083b3a9836a67ca9ff25d99872a57658a560
|
|
4
|
+
data.tar.gz: d1aefb9f16a5a842ad05d4e427b538a565240cf3c21969542bf614199e8e1955
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b87591c17a303bf820c2667f25ac802569173737c8bd63d13c86bc5aedf6e606cc50d3906c08d526dd8c9e934559f0cdc7717e8e6c4cfbd8ae5473ebc53bb9c
|
|
7
|
+
data.tar.gz: 9a6cafa4929f8a71b699d6ce1171e574bb6c99aca17b89850e7bf232d1269727d00f6a15f1b9a870f8319913e77b0ec865fc5633866161cb989425c9eefc76ce
|
data/.gitmodules
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-02-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Scheme-specific version comparison for Maven and NuGet via `Version.compare_with_scheme` and `Vers.compare_with_scheme`
|
|
14
|
+
- `Vers::MavenVersion` module with Maven qualifier ordering (alpha < beta < milestone < rc < snapshot < release < sp), digit/letter transitions, sublist rules, qualifier aliases, and trailing zero normalization
|
|
15
|
+
- `Vers::NuGetVersion` module with 4-part numeric versions, case-insensitive prereleases, and build metadata stripping
|
|
16
|
+
- `scheme` parameter threaded through `Interval`, `VersionRange`, `Constraint`, and `Parser` so Maven and NuGet ranges use their own comparison rules in `contains?`, `intersect`, and other interval operations
|
|
17
|
+
- 1010 conformance test cases from the vers-spec test suite for Maven and NuGet version comparison
|
|
18
|
+
|
|
10
19
|
## [1.0.3] - 2026-01-09
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -117,7 +126,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
117
126
|
- **No runtime dependencies** - pure Ruby implementation
|
|
118
127
|
- **Minitest** for testing (development dependency only)
|
|
119
128
|
|
|
120
|
-
[Unreleased]: https://github.com/andrew/vers/compare/v1.0
|
|
129
|
+
[Unreleased]: https://github.com/andrew/vers/compare/v1.1.0...HEAD
|
|
130
|
+
[1.1.0]: https://github.com/andrew/vers/compare/v1.0.3...v1.1.0
|
|
121
131
|
[1.0.3]: https://github.com/andrew/vers/compare/v1.0.2...v1.0.3
|
|
122
132
|
[1.0.2]: https://github.com/andrew/vers/compare/v1.0.1...v1.0.2
|
|
123
133
|
[1.0.1]: https://github.com/andrew/vers/compare/v1.0.0...v1.0.1
|
data/lib/vers/constraint.rb
CHANGED
|
@@ -93,21 +93,21 @@ module Vers
|
|
|
93
93
|
# Vers::Constraint.new(">=", "1.2.3").to_interval # => [1.2.3,+∞)
|
|
94
94
|
# Vers::Constraint.new("=", "1.0.0").to_interval # => [1.0.0,1.0.0]
|
|
95
95
|
#
|
|
96
|
-
def to_interval
|
|
96
|
+
def to_interval(scheme: nil)
|
|
97
97
|
case operator
|
|
98
98
|
when "="
|
|
99
|
-
Interval.exact(version)
|
|
99
|
+
Interval.exact(version, scheme: scheme)
|
|
100
100
|
when "!="
|
|
101
101
|
# != constraints need special handling in ranges - they create exclusions
|
|
102
102
|
nil
|
|
103
103
|
when ">"
|
|
104
|
-
Interval.greater_than(version, inclusive: false)
|
|
104
|
+
Interval.greater_than(version, inclusive: false, scheme: scheme)
|
|
105
105
|
when ">="
|
|
106
|
-
Interval.greater_than(version, inclusive: true)
|
|
106
|
+
Interval.greater_than(version, inclusive: true, scheme: scheme)
|
|
107
107
|
when "<"
|
|
108
|
-
Interval.less_than(version, inclusive: false)
|
|
108
|
+
Interval.less_than(version, inclusive: false, scheme: scheme)
|
|
109
109
|
when "<="
|
|
110
|
-
Interval.less_than(version, inclusive: true)
|
|
110
|
+
Interval.less_than(version, inclusive: true, scheme: scheme)
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
data/lib/vers/interval.rb
CHANGED
|
@@ -4,35 +4,36 @@ require_relative 'version'
|
|
|
4
4
|
|
|
5
5
|
module Vers
|
|
6
6
|
class Interval
|
|
7
|
-
attr_reader :min, :max, :min_inclusive, :max_inclusive
|
|
7
|
+
attr_reader :min, :max, :min_inclusive, :max_inclusive, :scheme
|
|
8
8
|
|
|
9
|
-
def initialize(min: nil, max: nil, min_inclusive: true, max_inclusive: true)
|
|
9
|
+
def initialize(min: nil, max: nil, min_inclusive: true, max_inclusive: true, scheme: nil)
|
|
10
10
|
@min = min
|
|
11
11
|
@max = max
|
|
12
12
|
@min_inclusive = min_inclusive
|
|
13
13
|
@max_inclusive = max_inclusive
|
|
14
|
+
@scheme = scheme
|
|
14
15
|
|
|
15
16
|
validate_bounds!
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def self.empty
|
|
19
|
-
new(min: "1", max: "0", min_inclusive: true, max_inclusive: true)
|
|
19
|
+
def self.empty(scheme: nil)
|
|
20
|
+
new(min: "1", max: "0", min_inclusive: true, max_inclusive: true, scheme: scheme)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
def self.unbounded
|
|
23
|
-
new
|
|
23
|
+
def self.unbounded(scheme: nil)
|
|
24
|
+
new(scheme: scheme)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
def self.exact(version)
|
|
27
|
-
new(min: version, max: version, min_inclusive: true, max_inclusive: true)
|
|
27
|
+
def self.exact(version, scheme: nil)
|
|
28
|
+
new(min: version, max: version, min_inclusive: true, max_inclusive: true, scheme: scheme)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
def self.greater_than(version, inclusive: false)
|
|
31
|
-
new(min: version, min_inclusive: inclusive)
|
|
31
|
+
def self.greater_than(version, inclusive: false, scheme: nil)
|
|
32
|
+
new(min: version, min_inclusive: inclusive, scheme: scheme)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def self.less_than(version, inclusive: false)
|
|
35
|
-
new(max: version, max_inclusive: inclusive)
|
|
35
|
+
def self.less_than(version, inclusive: false, scheme: nil)
|
|
36
|
+
new(max: version, max_inclusive: inclusive, scheme: scheme)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def empty?
|
|
@@ -59,7 +60,8 @@ module Vers
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def intersect(other)
|
|
62
|
-
|
|
63
|
+
merged_scheme = @scheme || other.scheme
|
|
64
|
+
return self.class.empty(scheme: merged_scheme) if empty? || other.empty?
|
|
63
65
|
|
|
64
66
|
new_min = nil
|
|
65
67
|
new_min_inclusive = true
|
|
@@ -110,7 +112,8 @@ module Vers
|
|
|
110
112
|
min: new_min,
|
|
111
113
|
max: new_max,
|
|
112
114
|
min_inclusive: new_min_inclusive,
|
|
113
|
-
max_inclusive: new_max_inclusive
|
|
115
|
+
max_inclusive: new_max_inclusive,
|
|
116
|
+
scheme: merged_scheme
|
|
114
117
|
)
|
|
115
118
|
end
|
|
116
119
|
|
|
@@ -120,6 +123,7 @@ module Vers
|
|
|
120
123
|
|
|
121
124
|
return nil unless overlaps?(other) || adjacent?(other)
|
|
122
125
|
|
|
126
|
+
merged_scheme = @scheme || other.scheme
|
|
123
127
|
new_min = nil
|
|
124
128
|
new_min_inclusive = true
|
|
125
129
|
new_max = nil
|
|
@@ -169,7 +173,8 @@ module Vers
|
|
|
169
173
|
min: new_min,
|
|
170
174
|
max: new_max,
|
|
171
175
|
min_inclusive: new_min_inclusive,
|
|
172
|
-
max_inclusive: new_max_inclusive
|
|
176
|
+
max_inclusive: new_max_inclusive,
|
|
177
|
+
scheme: merged_scheme
|
|
173
178
|
)
|
|
174
179
|
end
|
|
175
180
|
|
|
@@ -221,9 +226,12 @@ module Vers
|
|
|
221
226
|
return 0 if a == b
|
|
222
227
|
return -1 if a.nil?
|
|
223
228
|
return 1 if b.nil?
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
|
|
230
|
+
if @scheme
|
|
231
|
+
Version.compare_with_scheme(a, b, @scheme)
|
|
232
|
+
else
|
|
233
|
+
Version.compare(a, b)
|
|
234
|
+
end
|
|
227
235
|
end
|
|
228
236
|
end
|
|
229
237
|
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vers
|
|
4
|
+
module MavenVersion
|
|
5
|
+
MavenComponent = Struct.new(:is_numeric, :numeric, :qualifier, :is_null, :after_dash, keyword_init: true) do
|
|
6
|
+
def initialize(is_numeric: false, numeric: 0, qualifier: "", is_null: false, after_dash: false)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
QUALIFIER_ORDER = {
|
|
12
|
+
"alpha" => 1,
|
|
13
|
+
"beta" => 2,
|
|
14
|
+
"milestone" => 3,
|
|
15
|
+
"rc" => 4,
|
|
16
|
+
"snapshot" => 5,
|
|
17
|
+
"" => 6,
|
|
18
|
+
"sp" => 7
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
UNKNOWN_QUALIFIER_ORDER = 8
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def compare(a, b)
|
|
26
|
+
return 0 if a == b
|
|
27
|
+
|
|
28
|
+
parts_a = parse_maven_version(a)
|
|
29
|
+
parts_b = parse_maven_version(b)
|
|
30
|
+
|
|
31
|
+
max_len = [parts_a.length, parts_b.length].max
|
|
32
|
+
|
|
33
|
+
max_len.times do |i|
|
|
34
|
+
comp_a = i < parts_a.length ? parts_a[i] : MavenComponent.new(is_null: true)
|
|
35
|
+
comp_b = i < parts_b.length ? parts_b[i] : MavenComponent.new(is_null: true)
|
|
36
|
+
|
|
37
|
+
cmp = compare_components(comp_a, comp_b)
|
|
38
|
+
return cmp unless cmp == 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_maven_version(s)
|
|
45
|
+
s = s.downcase
|
|
46
|
+
|
|
47
|
+
parts, after_dash_flags = split_with_separators(s)
|
|
48
|
+
|
|
49
|
+
result = []
|
|
50
|
+
parts.each_with_index do |part, i|
|
|
51
|
+
next if part.empty?
|
|
52
|
+
|
|
53
|
+
next_is_digit = if i + 1 < parts.length
|
|
54
|
+
parts[i + 1].match?(/\A\d+\z/)
|
|
55
|
+
else
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
normalized = normalize_qualifier(part, next_is_digit)
|
|
60
|
+
next if normalized.empty?
|
|
61
|
+
|
|
62
|
+
after_dash = i < after_dash_flags.length ? after_dash_flags[i] : false
|
|
63
|
+
|
|
64
|
+
if normalized.match?(/\A\d+\z/)
|
|
65
|
+
result << MavenComponent.new(is_numeric: true, numeric: normalized.to_i, after_dash: after_dash)
|
|
66
|
+
else
|
|
67
|
+
result << MavenComponent.new(qualifier: normalized, after_dash: after_dash)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
normalize_components(result)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def split_with_separators(s)
|
|
75
|
+
parts = []
|
|
76
|
+
after_dash = []
|
|
77
|
+
current = +""
|
|
78
|
+
last_was_digit = false
|
|
79
|
+
first_char = true
|
|
80
|
+
current_after_dash = false
|
|
81
|
+
|
|
82
|
+
s.each_char do |c|
|
|
83
|
+
if c == "." || c == "-"
|
|
84
|
+
if current.length > 0
|
|
85
|
+
parts << current
|
|
86
|
+
after_dash << current_after_dash
|
|
87
|
+
current = +""
|
|
88
|
+
end
|
|
89
|
+
current_after_dash = (c == "-")
|
|
90
|
+
first_char = true
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
is_digit = c >= "0" && c <= "9"
|
|
95
|
+
|
|
96
|
+
if !first_char && is_digit != last_was_digit && current.length > 0
|
|
97
|
+
parts << current
|
|
98
|
+
after_dash << current_after_dash
|
|
99
|
+
current = +""
|
|
100
|
+
current_after_dash = true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
current << c
|
|
104
|
+
last_was_digit = is_digit
|
|
105
|
+
first_char = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if current.length > 0
|
|
109
|
+
parts << current
|
|
110
|
+
after_dash << current_after_dash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
[parts, after_dash]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def normalize_qualifier(q, next_is_digit)
|
|
117
|
+
if next_is_digit && q.length == 1
|
|
118
|
+
case q
|
|
119
|
+
when "a" then return "alpha"
|
|
120
|
+
when "b" then return "beta"
|
|
121
|
+
when "m" then return "milestone"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
case q
|
|
126
|
+
when "cr" then "rc"
|
|
127
|
+
when "ga", "final", "release" then ""
|
|
128
|
+
else q
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def normalize_components(components)
|
|
133
|
+
return components if components.empty?
|
|
134
|
+
|
|
135
|
+
first_sublist_idx = components.index { |c| c.after_dash }
|
|
136
|
+
|
|
137
|
+
if first_sublist_idx && first_sublist_idx > 0
|
|
138
|
+
base_end = first_sublist_idx
|
|
139
|
+
while base_end > 1 && components[base_end - 1].is_numeric && components[base_end - 1].numeric == 0
|
|
140
|
+
base_end -= 1
|
|
141
|
+
end
|
|
142
|
+
if base_end < first_sublist_idx
|
|
143
|
+
components = components[0...base_end] + components[first_sublist_idx..]
|
|
144
|
+
end
|
|
145
|
+
elsif first_sublist_idx.nil?
|
|
146
|
+
while components.length > 0 && components.last.is_numeric && components.last.numeric == 0
|
|
147
|
+
components.pop
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
components
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def compare_components(a, b)
|
|
155
|
+
return 0 if a.is_null && b.is_null
|
|
156
|
+
|
|
157
|
+
if a.is_null
|
|
158
|
+
return compare_to_null(b) * -1
|
|
159
|
+
end
|
|
160
|
+
if b.is_null
|
|
161
|
+
return compare_to_null(a)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if a.after_dash != b.after_dash
|
|
165
|
+
if a.after_dash
|
|
166
|
+
return b.is_numeric ? -1 : 1
|
|
167
|
+
else
|
|
168
|
+
return a.is_numeric ? 1 : -1
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if a.is_numeric && b.is_numeric
|
|
173
|
+
return a.numeric <=> b.numeric
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if a.is_numeric && !b.is_numeric
|
|
177
|
+
return 1
|
|
178
|
+
end
|
|
179
|
+
if !a.is_numeric && b.is_numeric
|
|
180
|
+
return -1
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
order_a = qualifier_order(a.qualifier)
|
|
184
|
+
order_b = qualifier_order(b.qualifier)
|
|
185
|
+
|
|
186
|
+
if order_a != order_b
|
|
187
|
+
return order_a <=> order_b
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
known_a = QUALIFIER_ORDER.key?(a.qualifier)
|
|
191
|
+
known_b = QUALIFIER_ORDER.key?(b.qualifier)
|
|
192
|
+
|
|
193
|
+
if !known_a && !known_b
|
|
194
|
+
return a.qualifier <=> b.qualifier
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
0
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def compare_to_null(comp)
|
|
201
|
+
if comp.is_numeric
|
|
202
|
+
if comp.numeric == 0
|
|
203
|
+
0
|
|
204
|
+
else
|
|
205
|
+
1
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
order_comp = qualifier_order(comp.qualifier)
|
|
209
|
+
order_null = QUALIFIER_ORDER[""]
|
|
210
|
+
order_comp <=> order_null
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def qualifier_order(q)
|
|
215
|
+
QUALIFIER_ORDER.fetch(q, UNKNOWN_QUALIFIER_ORDER)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vers
|
|
4
|
+
module NuGetVersion
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def compare(a, b)
|
|
8
|
+
return 0 if a == b
|
|
9
|
+
|
|
10
|
+
parts_a = parse_nuget(a)
|
|
11
|
+
parts_b = parse_nuget(b)
|
|
12
|
+
|
|
13
|
+
4.times do |i|
|
|
14
|
+
cmp = parts_a[:numeric][i] <=> parts_b[:numeric][i]
|
|
15
|
+
return cmp unless cmp == 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
pre_a = parts_a[:prerelease]
|
|
19
|
+
pre_b = parts_b[:prerelease]
|
|
20
|
+
|
|
21
|
+
return 1 if pre_a.empty? && !pre_b.empty?
|
|
22
|
+
return -1 if !pre_a.empty? && pre_b.empty?
|
|
23
|
+
return 0 if pre_a.empty? && pre_b.empty?
|
|
24
|
+
|
|
25
|
+
compare_prerelease(pre_a, pre_b)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_nuget(s)
|
|
29
|
+
s = s.dup
|
|
30
|
+
|
|
31
|
+
if (idx = s.index("+"))
|
|
32
|
+
s = s[0...idx]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
prerelease = ""
|
|
36
|
+
if (idx = s.index("-"))
|
|
37
|
+
prerelease = s[(idx + 1)..]
|
|
38
|
+
s = s[0...idx]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
numeric = [0, 0, 0, 0]
|
|
42
|
+
parts = s.split(".")
|
|
43
|
+
parts.each_with_index do |part, i|
|
|
44
|
+
break if i >= 4
|
|
45
|
+
numeric[i] = part.to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
{ numeric: numeric, prerelease: prerelease }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compare_prerelease(a, b)
|
|
52
|
+
parts_a = a.downcase.split(".")
|
|
53
|
+
parts_b = b.downcase.split(".")
|
|
54
|
+
|
|
55
|
+
max_len = [parts_a.length, parts_b.length].max
|
|
56
|
+
|
|
57
|
+
max_len.times do |i|
|
|
58
|
+
part_a = i < parts_a.length ? parts_a[i] : nil
|
|
59
|
+
part_b = i < parts_b.length ? parts_b[i] : nil
|
|
60
|
+
|
|
61
|
+
return -1 if part_a.nil?
|
|
62
|
+
return 1 if part_b.nil?
|
|
63
|
+
|
|
64
|
+
num_a = part_a.match?(/\A\d+\z/) ? part_a.to_i : nil
|
|
65
|
+
num_b = part_b.match?(/\A\d+\z/) ? part_b.to_i : nil
|
|
66
|
+
|
|
67
|
+
if num_a && num_b
|
|
68
|
+
cmp = num_a <=> num_b
|
|
69
|
+
return cmp unless cmp == 0
|
|
70
|
+
else
|
|
71
|
+
cmp = part_a <=> part_b
|
|
72
|
+
return cmp unless cmp == 0
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
0
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/vers/parser.rb
CHANGED
|
@@ -83,8 +83,14 @@ module Vers
|
|
|
83
83
|
parse_pypi_range(range_string)
|
|
84
84
|
when "maven"
|
|
85
85
|
parse_maven_range(range_string)
|
|
86
|
+
when "cargo"
|
|
87
|
+
parse_npm_range(range_string)
|
|
86
88
|
when "nuget"
|
|
87
89
|
parse_nuget_range(range_string)
|
|
90
|
+
when "hex", "elixir"
|
|
91
|
+
parse_hex_range(range_string)
|
|
92
|
+
when "go", "golang"
|
|
93
|
+
parse_go_range(range_string)
|
|
88
94
|
when "deb", "debian"
|
|
89
95
|
parse_debian_range(range_string)
|
|
90
96
|
when "rpm"
|
|
@@ -103,22 +109,33 @@ module Vers
|
|
|
103
109
|
# @return [String] The vers URI string
|
|
104
110
|
#
|
|
105
111
|
def to_vers_string(version_range, scheme)
|
|
106
|
-
return "
|
|
112
|
+
return "vers:#{scheme}/*" if version_range.unbounded?
|
|
107
113
|
return "vers:#{scheme}/" if version_range.empty?
|
|
108
114
|
|
|
115
|
+
intervals = version_range.raw_constraints || version_range.intervals
|
|
109
116
|
constraints = []
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
|
|
128
|
+
intervals.each do |interval|
|
|
112
129
|
if interval.min == interval.max && interval.min_inclusive && interval.max_inclusive
|
|
113
130
|
# Exact version
|
|
114
|
-
constraints <<
|
|
131
|
+
constraints << interval.min.to_s
|
|
115
132
|
else
|
|
116
133
|
# Range constraints
|
|
117
134
|
if interval.min
|
|
118
135
|
operator = interval.min_inclusive ? ">=" : ">"
|
|
119
136
|
constraints << "#{operator}#{interval.min}"
|
|
120
137
|
end
|
|
121
|
-
|
|
138
|
+
|
|
122
139
|
if interval.max
|
|
123
140
|
operator = interval.max_inclusive ? "<=" : "<"
|
|
124
141
|
constraints << "#{operator}#{interval.max}"
|
|
@@ -126,30 +143,45 @@ module Vers
|
|
|
126
143
|
end
|
|
127
144
|
end
|
|
128
145
|
|
|
146
|
+
constraints.sort_by! { |c| sort_key_for_constraint(c) }
|
|
147
|
+
|
|
129
148
|
"vers:#{scheme}/#{constraints.join('|')}"
|
|
130
149
|
end
|
|
131
150
|
|
|
132
151
|
private
|
|
133
152
|
|
|
153
|
+
def sort_key_for_constraint(constraint)
|
|
154
|
+
version = constraint.sub(/\A[><=!]+/, '')
|
|
155
|
+
v = Version.cached_new(version)
|
|
156
|
+
[v, constraint]
|
|
157
|
+
end
|
|
158
|
+
|
|
134
159
|
def parse_constraints(constraints_string, scheme)
|
|
135
|
-
constraint_strings = constraints_string.split(
|
|
160
|
+
constraint_strings = constraints_string.split(/[|,]/)
|
|
136
161
|
intervals = []
|
|
137
162
|
exclusions = []
|
|
163
|
+
interval_scheme = %w[maven nuget].include?(scheme) ? scheme : nil
|
|
138
164
|
|
|
139
165
|
constraint_strings.each do |constraint_string|
|
|
140
166
|
constraint = Constraint.parse(constraint_string.strip)
|
|
141
|
-
|
|
167
|
+
|
|
142
168
|
if constraint.exclusion?
|
|
143
169
|
exclusions << constraint.version
|
|
144
170
|
else
|
|
145
|
-
interval = constraint.to_interval
|
|
171
|
+
interval = constraint.to_interval(scheme: interval_scheme)
|
|
146
172
|
intervals << interval if interval
|
|
147
173
|
end
|
|
148
174
|
end
|
|
149
175
|
|
|
150
|
-
# Start with the union of all positive constraints
|
|
151
|
-
range =
|
|
152
|
-
|
|
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
|
|
184
|
+
|
|
153
185
|
# Apply exclusions
|
|
154
186
|
exclusions.each do |version|
|
|
155
187
|
range = range.exclude(version)
|
|
@@ -168,7 +200,7 @@ module Vers
|
|
|
168
200
|
# Handle || (OR) operator
|
|
169
201
|
if range_string.include?('||')
|
|
170
202
|
or_parts = range_string.split('||').map(&:strip)
|
|
171
|
-
ranges = or_parts.map { |part|
|
|
203
|
+
ranges = or_parts.map { |part| parse_npm_range(part) }
|
|
172
204
|
return ranges.reduce { |acc, range| acc.union(range) }
|
|
173
205
|
end
|
|
174
206
|
|
|
@@ -179,8 +211,23 @@ module Vers
|
|
|
179
211
|
|
|
180
212
|
# Handle space-separated AND constraints
|
|
181
213
|
and_parts = range_string.split(/\s+/).reject(&:empty?)
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
# Re-join bare operators with their version
|
|
215
|
+
merged = []
|
|
216
|
+
and_parts.each do |part|
|
|
217
|
+
if merged.last&.match?(/\A(>=|<=|!=|[<>=~^])\z/)
|
|
218
|
+
merged[-1] = "#{merged.last}#{part}"
|
|
219
|
+
else
|
|
220
|
+
merged << part
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
ranges = merged.map { |part| parse_npm_single_range(part) }
|
|
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
|
|
184
231
|
end
|
|
185
232
|
|
|
186
233
|
def parse_npm_single_range(range_string)
|
|
@@ -230,8 +277,25 @@ module Vers
|
|
|
230
277
|
# Invalid patterns that should raise errors
|
|
231
278
|
raise ArgumentError, "Invalid NPM range format: #{range_string}"
|
|
232
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
|
|
233
294
|
# Standard constraint
|
|
234
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)
|
|
235
299
|
if constraint.exclusion?
|
|
236
300
|
VersionRange.unbounded.exclude(constraint.version)
|
|
237
301
|
else
|
|
@@ -263,9 +327,27 @@ module Vers
|
|
|
263
327
|
|
|
264
328
|
def parse_tilde_range(version)
|
|
265
329
|
v = Version.cached_new(version)
|
|
266
|
-
|
|
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
|
|
267
346
|
# ~1.2.3 := >=1.2.3 <1.3.0
|
|
268
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"
|
|
269
351
|
else
|
|
270
352
|
# ~1 := >=1.0.0 <2.0.0
|
|
271
353
|
"#{v.major + 1}.0.0"
|
|
@@ -292,14 +374,14 @@ module Vers
|
|
|
292
374
|
def parse_pessimistic_range(version)
|
|
293
375
|
v = Version.cached_new(version)
|
|
294
376
|
upper_version = if v.patch
|
|
295
|
-
# ~> 1.2.3 := >= 1.2.3, < 1.3
|
|
296
|
-
"#{v.major}.#{v.minor + 1}
|
|
377
|
+
# ~> 1.2.3 := >= 1.2.3, < 1.3
|
|
378
|
+
"#{v.major}.#{v.minor + 1}"
|
|
297
379
|
elsif v.minor
|
|
298
|
-
# ~> 1.2 := >= 1.2.0, < 2
|
|
299
|
-
"#{v.major + 1}
|
|
380
|
+
# ~> 1.2 := >= 1.2.0, < 2
|
|
381
|
+
"#{v.major + 1}"
|
|
300
382
|
else
|
|
301
|
-
# ~> 1 := >= 1.0.0, < 2
|
|
302
|
-
"#{v.major + 1}
|
|
383
|
+
# ~> 1 := >= 1.0.0, < 2
|
|
384
|
+
"#{v.major + 1}"
|
|
303
385
|
end
|
|
304
386
|
|
|
305
387
|
VersionRange.new([
|
|
@@ -323,70 +405,70 @@ module Vers
|
|
|
323
405
|
raise ArgumentError, "Malformed Maven range: mismatched brackets in '#{range_string}'"
|
|
324
406
|
end
|
|
325
407
|
end
|
|
326
|
-
|
|
408
|
+
|
|
327
409
|
case range_string
|
|
328
410
|
when /^\[([^,]+),([^,]+)\]$/
|
|
329
411
|
# [1.0,2.0] := >=1.0 <=2.0
|
|
330
412
|
min_version = Regexp.last_match(1).strip
|
|
331
413
|
max_version = Regexp.last_match(2).strip
|
|
332
414
|
VersionRange.new([
|
|
333
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true)
|
|
334
|
-
])
|
|
415
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: true, scheme: "maven")
|
|
416
|
+
], scheme: "maven")
|
|
335
417
|
when /^\(([^,]+),([^,]+)\)$/
|
|
336
418
|
# (1.0,2.0) := >1.0 <2.0
|
|
337
419
|
min_version = Regexp.last_match(1).strip
|
|
338
420
|
max_version = Regexp.last_match(2).strip
|
|
339
421
|
VersionRange.new([
|
|
340
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false)
|
|
341
|
-
])
|
|
422
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: false, scheme: "maven")
|
|
423
|
+
], scheme: "maven")
|
|
342
424
|
when /^\[([^,]+),([^,]+)\)$/
|
|
343
425
|
# [1.0,2.0) := >=1.0 <2.0
|
|
344
426
|
min_version = Regexp.last_match(1).strip
|
|
345
427
|
max_version = Regexp.last_match(2).strip
|
|
346
428
|
VersionRange.new([
|
|
347
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false)
|
|
348
|
-
])
|
|
429
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: true, max_inclusive: false, scheme: "maven")
|
|
430
|
+
], scheme: "maven")
|
|
349
431
|
when /^\(([^,]+),([^,]+)\]$/
|
|
350
432
|
# (1.0,2.0] := >1.0 <=2.0
|
|
351
433
|
min_version = Regexp.last_match(1).strip
|
|
352
434
|
max_version = Regexp.last_match(2).strip
|
|
353
435
|
VersionRange.new([
|
|
354
|
-
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true)
|
|
355
|
-
])
|
|
436
|
+
Interval.new(min: min_version, max: max_version, min_inclusive: false, max_inclusive: true, scheme: "maven")
|
|
437
|
+
], scheme: "maven")
|
|
356
438
|
when /^\[([^,]+)\]$/
|
|
357
439
|
# [1.0] := exactly 1.0
|
|
358
440
|
version = Regexp.last_match(1).strip
|
|
359
|
-
VersionRange.exact(version)
|
|
441
|
+
VersionRange.exact(version, scheme: "maven")
|
|
360
442
|
when /^\[([^,]+),\)$/
|
|
361
443
|
# [1.0,) := >=1.0
|
|
362
444
|
min_version = Regexp.last_match(1).strip
|
|
363
445
|
VersionRange.new([
|
|
364
|
-
Interval.new(min: min_version, min_inclusive: true)
|
|
365
|
-
])
|
|
446
|
+
Interval.new(min: min_version, min_inclusive: true, scheme: "maven")
|
|
447
|
+
], scheme: "maven")
|
|
366
448
|
when /^\(([^,]+),\)$/
|
|
367
449
|
# (1.0,) := >1.0
|
|
368
450
|
min_version = Regexp.last_match(1).strip
|
|
369
451
|
VersionRange.new([
|
|
370
|
-
Interval.new(min: min_version, min_inclusive: false)
|
|
371
|
-
])
|
|
452
|
+
Interval.new(min: min_version, min_inclusive: false, scheme: "maven")
|
|
453
|
+
], scheme: "maven")
|
|
372
454
|
when /^\(,([^,]+)\]$/
|
|
373
455
|
# (,1.0] := <=1.0
|
|
374
456
|
max_version = Regexp.last_match(1).strip
|
|
375
457
|
VersionRange.new([
|
|
376
|
-
Interval.new(max: max_version, max_inclusive: true)
|
|
377
|
-
])
|
|
458
|
+
Interval.new(max: max_version, max_inclusive: true, scheme: "maven")
|
|
459
|
+
], scheme: "maven")
|
|
378
460
|
when /^\(,([^,]+)\)$/
|
|
379
461
|
# (,1.0) := <1.0
|
|
380
462
|
max_version = Regexp.last_match(1).strip
|
|
381
463
|
VersionRange.new([
|
|
382
|
-
Interval.new(max: max_version, max_inclusive: false)
|
|
383
|
-
])
|
|
464
|
+
Interval.new(max: max_version, max_inclusive: false, scheme: "maven")
|
|
465
|
+
], scheme: "maven")
|
|
384
466
|
when /^[0-9]/
|
|
385
467
|
# Simple version number without brackets - in Maven, this is minimum version
|
|
386
468
|
if range_string.match(/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9.-]+)?$/)
|
|
387
469
|
VersionRange.new([
|
|
388
|
-
Interval.new(min: range_string, min_inclusive: true)
|
|
389
|
-
])
|
|
470
|
+
Interval.new(min: range_string, min_inclusive: true, scheme: "maven")
|
|
471
|
+
], scheme: "maven")
|
|
390
472
|
else
|
|
391
473
|
parse_constraints(range_string, 'maven')
|
|
392
474
|
end
|
|
@@ -400,7 +482,7 @@ module Vers
|
|
|
400
482
|
# Find all individual ranges by splitting on comma between brackets
|
|
401
483
|
individual_ranges = []
|
|
402
484
|
remaining = range_string.strip
|
|
403
|
-
|
|
485
|
+
|
|
404
486
|
while remaining.length > 0
|
|
405
487
|
# Find the next complete bracket range
|
|
406
488
|
if match = remaining.match(/^[\[\(][^\[\]\(\)]*[\]\)]/)
|
|
@@ -412,7 +494,7 @@ module Vers
|
|
|
412
494
|
break
|
|
413
495
|
end
|
|
414
496
|
end
|
|
415
|
-
|
|
497
|
+
|
|
416
498
|
if individual_ranges.length > 1
|
|
417
499
|
individual_ranges.each do |range_part|
|
|
418
500
|
begin
|
|
@@ -422,13 +504,13 @@ module Vers
|
|
|
422
504
|
# If parsing fails, skip this part
|
|
423
505
|
end
|
|
424
506
|
end
|
|
425
|
-
|
|
507
|
+
|
|
426
508
|
if ranges.any?
|
|
427
509
|
return ranges.reduce { |acc, range| acc.union(range) }
|
|
428
510
|
end
|
|
429
511
|
end
|
|
430
512
|
end
|
|
431
|
-
|
|
513
|
+
|
|
432
514
|
# Fall back to standard constraint parsing
|
|
433
515
|
parse_constraints(range_string, 'maven')
|
|
434
516
|
else
|
|
@@ -443,19 +525,147 @@ module Vers
|
|
|
443
525
|
# But simple version strings like "1.0" are minimum versions, not exact
|
|
444
526
|
case range_string
|
|
445
527
|
when /^[\[\(].+[\]\)]$/
|
|
446
|
-
#
|
|
447
|
-
|
|
528
|
+
# Parse bracket notation like Maven but with nuget scheme
|
|
529
|
+
range = parse_nuget_bracket_range(range_string)
|
|
530
|
+
range
|
|
448
531
|
when /^[0-9]/
|
|
449
532
|
# Simple version number - treat as minimum version for NuGet
|
|
450
533
|
VersionRange.new([
|
|
451
|
-
Interval.new(min: range_string, min_inclusive: true)
|
|
452
|
-
])
|
|
534
|
+
Interval.new(min: range_string, min_inclusive: true, scheme: "nuget")
|
|
535
|
+
], scheme: "nuget")
|
|
453
536
|
else
|
|
454
537
|
# Fall back to standard constraint parsing
|
|
455
538
|
parse_constraints(range_string, 'nuget')
|
|
456
539
|
end
|
|
457
540
|
end
|
|
458
541
|
|
|
542
|
+
def parse_nuget_bracket_range(range_string)
|
|
543
|
+
case range_string
|
|
544
|
+
when /^\[([^,]+),([^,]+)\]$/
|
|
545
|
+
min_v = Regexp.last_match(1).strip
|
|
546
|
+
max_v = Regexp.last_match(2).strip
|
|
547
|
+
VersionRange.new([
|
|
548
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: true, max_inclusive: true, scheme: "nuget")
|
|
549
|
+
], scheme: "nuget")
|
|
550
|
+
when /^\(([^,]+),([^,]+)\)$/
|
|
551
|
+
min_v = Regexp.last_match(1).strip
|
|
552
|
+
max_v = Regexp.last_match(2).strip
|
|
553
|
+
VersionRange.new([
|
|
554
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: false, max_inclusive: false, scheme: "nuget")
|
|
555
|
+
], scheme: "nuget")
|
|
556
|
+
when /^\[([^,]+),([^,]+)\)$/
|
|
557
|
+
min_v = Regexp.last_match(1).strip
|
|
558
|
+
max_v = Regexp.last_match(2).strip
|
|
559
|
+
VersionRange.new([
|
|
560
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: true, max_inclusive: false, scheme: "nuget")
|
|
561
|
+
], scheme: "nuget")
|
|
562
|
+
when /^\(([^,]+),([^,]+)\]$/
|
|
563
|
+
min_v = Regexp.last_match(1).strip
|
|
564
|
+
max_v = Regexp.last_match(2).strip
|
|
565
|
+
VersionRange.new([
|
|
566
|
+
Interval.new(min: min_v, max: max_v, min_inclusive: false, max_inclusive: true, scheme: "nuget")
|
|
567
|
+
], scheme: "nuget")
|
|
568
|
+
when /^\[([^,]+)\]$/
|
|
569
|
+
version = Regexp.last_match(1).strip
|
|
570
|
+
VersionRange.exact(version, scheme: "nuget")
|
|
571
|
+
when /^\[([^,]+),\)$/
|
|
572
|
+
min_v = Regexp.last_match(1).strip
|
|
573
|
+
VersionRange.new([
|
|
574
|
+
Interval.new(min: min_v, min_inclusive: true, scheme: "nuget")
|
|
575
|
+
], scheme: "nuget")
|
|
576
|
+
when /^\(([^,]+),\)$/
|
|
577
|
+
min_v = Regexp.last_match(1).strip
|
|
578
|
+
VersionRange.new([
|
|
579
|
+
Interval.new(min: min_v, min_inclusive: false, scheme: "nuget")
|
|
580
|
+
], scheme: "nuget")
|
|
581
|
+
when /^\(,([^,]+)\]$/
|
|
582
|
+
max_v = Regexp.last_match(1).strip
|
|
583
|
+
VersionRange.new([
|
|
584
|
+
Interval.new(max: max_v, max_inclusive: true, scheme: "nuget")
|
|
585
|
+
], scheme: "nuget")
|
|
586
|
+
when /^\(,([^,]+)\)$/
|
|
587
|
+
max_v = Regexp.last_match(1).strip
|
|
588
|
+
VersionRange.new([
|
|
589
|
+
Interval.new(max: max_v, max_inclusive: false, scheme: "nuget")
|
|
590
|
+
], scheme: "nuget")
|
|
591
|
+
else
|
|
592
|
+
parse_constraints(range_string, 'nuget')
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Hex/Elixir range parsing
|
|
597
|
+
def parse_hex_range(range_string)
|
|
598
|
+
# Handle "or" disjunction first
|
|
599
|
+
if range_string.include?(" or ")
|
|
600
|
+
or_parts = range_string.split(" or ").map(&:strip)
|
|
601
|
+
ranges = or_parts.map { |part| parse_hex_single_range(part) }
|
|
602
|
+
return ranges.reduce { |acc, range| acc.union(range) }
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
parse_hex_single_range(range_string)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def parse_hex_single_range(range_string)
|
|
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?)
|
|
612
|
+
ranges = and_parts.map { |part| parse_hex_constraint(part) }
|
|
613
|
+
return ranges.reduce { |acc, range| acc.intersect(range) }
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
parse_hex_constraint(range_string)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def parse_hex_constraint(constraint_string)
|
|
620
|
+
if constraint_string.match(/^~>\s*(.+)$/)
|
|
621
|
+
parse_pessimistic_range(Regexp.last_match(1).strip)
|
|
622
|
+
else
|
|
623
|
+
# Normalize == to = for our internal constraint parsing
|
|
624
|
+
normalized = constraint_string.gsub("==", "=")
|
|
625
|
+
constraint = Constraint.parse(normalized.strip)
|
|
626
|
+
if constraint.exclusion?
|
|
627
|
+
VersionRange.unbounded.exclude(constraint.version)
|
|
628
|
+
else
|
|
629
|
+
VersionRange.new([constraint.to_interval])
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Go module range parsing (comma-separated AND constraints, v-prefix preserved)
|
|
635
|
+
def parse_go_range(range_string)
|
|
636
|
+
return VersionRange.unbounded if range_string.nil? || range_string.strip.empty?
|
|
637
|
+
|
|
638
|
+
unless range_string.include?(',')
|
|
639
|
+
return parse_constraints(range_string, 'go')
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
parts = range_string.split(',').map(&:strip)
|
|
643
|
+
constraint_intervals = []
|
|
644
|
+
exclusions = []
|
|
645
|
+
|
|
646
|
+
parts.each do |part|
|
|
647
|
+
constraint = Constraint.parse(part)
|
|
648
|
+
if constraint.exclusion?
|
|
649
|
+
exclusions << constraint.version
|
|
650
|
+
else
|
|
651
|
+
interval = constraint.to_interval
|
|
652
|
+
constraint_intervals << interval if interval
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
if constraint_intervals.any?
|
|
657
|
+
range = VersionRange.new([constraint_intervals.first])
|
|
658
|
+
constraint_intervals[1..].each do |interval|
|
|
659
|
+
range = range.intersect(VersionRange.new([interval]))
|
|
660
|
+
end
|
|
661
|
+
else
|
|
662
|
+
range = VersionRange.unbounded
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
exclusions.each { |version| range = range.exclude(version) }
|
|
666
|
+
range
|
|
667
|
+
end
|
|
668
|
+
|
|
459
669
|
# Debian range parsing
|
|
460
670
|
def parse_debian_range(range_string)
|
|
461
671
|
# Debian uses operators like >=, <=, =, >>, <<
|
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.0
|
|
4
|
+
VERSION = "1.2.0"
|
|
5
5
|
|
|
6
6
|
##
|
|
7
7
|
# Handles version comparison and normalization across different package ecosystems.
|
|
@@ -64,10 +64,29 @@ module Vers
|
|
|
64
64
|
# Use cached versions for better performance
|
|
65
65
|
version_a = cached_new(a)
|
|
66
66
|
version_b = cached_new(b)
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
version_a <=> version_b
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
##
|
|
72
|
+
# Compares two version strings using scheme-specific rules
|
|
73
|
+
#
|
|
74
|
+
# @param a [String] First version string
|
|
75
|
+
# @param b [String] Second version string
|
|
76
|
+
# @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
|
|
77
|
+
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
|
78
|
+
#
|
|
79
|
+
def self.compare_with_scheme(a, b, scheme)
|
|
80
|
+
case scheme
|
|
81
|
+
when "maven"
|
|
82
|
+
MavenVersion.compare(a, b)
|
|
83
|
+
when "nuget"
|
|
84
|
+
NuGetVersion.compare(a, b)
|
|
85
|
+
else
|
|
86
|
+
compare(a, b)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
71
90
|
##
|
|
72
91
|
# Normalizes a version string to a consistent format
|
|
73
92
|
#
|
|
@@ -85,10 +104,12 @@ module Vers
|
|
|
85
104
|
# @return [Boolean] true if the version is valid
|
|
86
105
|
#
|
|
87
106
|
def self.valid?(version_string)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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/, '')
|
|
92
113
|
end
|
|
93
114
|
|
|
94
115
|
##
|
|
@@ -295,6 +316,9 @@ module Vers
|
|
|
295
316
|
private
|
|
296
317
|
|
|
297
318
|
def parse_version
|
|
319
|
+
# Strip leading v/V prefix (e.g. "v1.0.0" -> "1.0.0")
|
|
320
|
+
@original = @original.sub(/\Av/i, '')
|
|
321
|
+
|
|
298
322
|
# Handle simple numeric versions (optimized case)
|
|
299
323
|
if @original.match(/^\d+$/)
|
|
300
324
|
@major = @original.to_i
|
data/lib/vers/version_range.rb
CHANGED
|
@@ -5,31 +5,38 @@ require_relative 'version'
|
|
|
5
5
|
|
|
6
6
|
module Vers
|
|
7
7
|
class VersionRange
|
|
8
|
-
attr_reader :intervals
|
|
9
|
-
|
|
10
|
-
def initialize(intervals = [])
|
|
11
|
-
@
|
|
8
|
+
attr_reader :intervals, :raw_constraints, :scheme
|
|
9
|
+
|
|
10
|
+
def initialize(intervals = [], raw_constraints: nil, scheme: nil)
|
|
11
|
+
@scheme = scheme
|
|
12
|
+
@intervals = intervals.compact.reject(&:empty?)
|
|
13
|
+
if @scheme
|
|
14
|
+
@intervals.sort! { |a, b| compare_interval_bounds(a, b) }
|
|
15
|
+
else
|
|
16
|
+
@intervals.sort_by! { |i| [i.min || '', i.max || ''] }
|
|
17
|
+
end
|
|
18
|
+
@raw_constraints = raw_constraints
|
|
12
19
|
merge_overlapping_intervals!
|
|
13
20
|
end
|
|
14
21
|
|
|
15
|
-
def self.empty
|
|
16
|
-
new([])
|
|
22
|
+
def self.empty(scheme: nil)
|
|
23
|
+
new([], scheme: scheme)
|
|
17
24
|
end
|
|
18
25
|
|
|
19
|
-
def self.unbounded
|
|
20
|
-
new([Interval.unbounded])
|
|
26
|
+
def self.unbounded(scheme: nil)
|
|
27
|
+
new([Interval.unbounded(scheme: scheme)], scheme: scheme)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
|
-
def self.exact(version)
|
|
24
|
-
new([Interval.exact(version)])
|
|
30
|
+
def self.exact(version, scheme: nil)
|
|
31
|
+
new([Interval.exact(version, scheme: scheme)], scheme: scheme)
|
|
25
32
|
end
|
|
26
33
|
|
|
27
|
-
def self.greater_than(version, inclusive: false)
|
|
28
|
-
new([Interval.greater_than(version, inclusive: inclusive)])
|
|
34
|
+
def self.greater_than(version, inclusive: false, scheme: nil)
|
|
35
|
+
new([Interval.greater_than(version, inclusive: inclusive, scheme: scheme)], scheme: scheme)
|
|
29
36
|
end
|
|
30
37
|
|
|
31
|
-
def self.less_than(version, inclusive: false)
|
|
32
|
-
new([Interval.less_than(version, inclusive: inclusive)])
|
|
38
|
+
def self.less_than(version, inclusive: false, scheme: nil)
|
|
39
|
+
new([Interval.less_than(version, inclusive: inclusive, scheme: scheme)], scheme: scheme)
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def empty?
|
|
@@ -45,38 +52,47 @@ module Vers
|
|
|
45
52
|
end
|
|
46
53
|
|
|
47
54
|
def intersect(other)
|
|
55
|
+
merged_scheme = @scheme || other.scheme
|
|
48
56
|
result_intervals = []
|
|
49
|
-
|
|
57
|
+
|
|
50
58
|
intervals.each do |interval1|
|
|
51
59
|
other.intervals.each do |interval2|
|
|
52
60
|
intersection = interval1.intersect(interval2)
|
|
53
61
|
result_intervals << intersection unless intersection.empty?
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
|
|
65
|
+
combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals)
|
|
66
|
+
self.class.new(result_intervals, raw_constraints: combined_raw, scheme: merged_scheme)
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
def union(other)
|
|
61
|
-
|
|
70
|
+
merged_scheme = @scheme || other.scheme
|
|
71
|
+
combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals)
|
|
72
|
+
self.class.new(intervals + other.intervals, raw_constraints: combined_raw, scheme: merged_scheme)
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
def complement
|
|
65
|
-
return self.class.unbounded if empty?
|
|
66
|
-
return self.class.empty if unbounded?
|
|
76
|
+
return self.class.unbounded(scheme: @scheme) if empty?
|
|
77
|
+
return self.class.empty(scheme: @scheme) if unbounded?
|
|
67
78
|
|
|
68
79
|
result_intervals = []
|
|
69
|
-
|
|
70
|
-
sorted_intervals =
|
|
71
|
-
|
|
80
|
+
|
|
81
|
+
sorted_intervals = if @scheme
|
|
82
|
+
intervals.sort { |a, b| compare_interval_bounds(a, b) }
|
|
83
|
+
else
|
|
84
|
+
intervals.sort_by { |i| i.min || '' }
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
first_interval = sorted_intervals.first
|
|
73
88
|
if first_interval.min
|
|
74
89
|
result_intervals << Interval.new(
|
|
75
90
|
max: first_interval.min,
|
|
76
|
-
max_inclusive: !first_interval.min_inclusive
|
|
91
|
+
max_inclusive: !first_interval.min_inclusive,
|
|
92
|
+
scheme: @scheme
|
|
77
93
|
)
|
|
78
94
|
end
|
|
79
|
-
|
|
95
|
+
|
|
80
96
|
sorted_intervals.each_cons(2) do |curr, next_interval|
|
|
81
97
|
if curr.max && next_interval.min
|
|
82
98
|
comparison = version_compare(curr.max, next_interval.min)
|
|
@@ -85,53 +101,57 @@ module Vers
|
|
|
85
101
|
min: curr.max,
|
|
86
102
|
max: next_interval.min,
|
|
87
103
|
min_inclusive: !curr.max_inclusive,
|
|
88
|
-
max_inclusive: !next_interval.min_inclusive
|
|
104
|
+
max_inclusive: !next_interval.min_inclusive,
|
|
105
|
+
scheme: @scheme
|
|
89
106
|
)
|
|
90
107
|
end
|
|
91
108
|
end
|
|
92
109
|
end
|
|
93
|
-
|
|
110
|
+
|
|
94
111
|
last_interval = sorted_intervals.last
|
|
95
112
|
if last_interval.max
|
|
96
113
|
result_intervals << Interval.new(
|
|
97
114
|
min: last_interval.max,
|
|
98
|
-
min_inclusive: !last_interval.max_inclusive
|
|
115
|
+
min_inclusive: !last_interval.max_inclusive,
|
|
116
|
+
scheme: @scheme
|
|
99
117
|
)
|
|
100
118
|
end
|
|
101
|
-
|
|
102
|
-
self.class.new(result_intervals)
|
|
119
|
+
|
|
120
|
+
self.class.new(result_intervals, scheme: @scheme)
|
|
103
121
|
end
|
|
104
122
|
|
|
105
123
|
def exclude(version)
|
|
106
124
|
return self if !contains?(version)
|
|
107
|
-
|
|
125
|
+
|
|
108
126
|
result_intervals = []
|
|
109
|
-
|
|
127
|
+
|
|
110
128
|
intervals.each do |interval|
|
|
111
129
|
if interval.contains?(version)
|
|
112
|
-
if interval.min
|
|
130
|
+
if interval.min.nil? || version_compare(interval.min, version) < 0
|
|
113
131
|
result_intervals << Interval.new(
|
|
114
132
|
min: interval.min,
|
|
115
133
|
max: version,
|
|
116
134
|
min_inclusive: interval.min_inclusive,
|
|
117
|
-
max_inclusive: false
|
|
135
|
+
max_inclusive: false,
|
|
136
|
+
scheme: @scheme
|
|
118
137
|
)
|
|
119
138
|
end
|
|
120
|
-
|
|
121
|
-
if interval.max
|
|
139
|
+
|
|
140
|
+
if interval.max.nil? || version_compare(version, interval.max) < 0
|
|
122
141
|
result_intervals << Interval.new(
|
|
123
142
|
min: version,
|
|
124
143
|
max: interval.max,
|
|
125
144
|
min_inclusive: false,
|
|
126
|
-
max_inclusive: interval.max_inclusive
|
|
145
|
+
max_inclusive: interval.max_inclusive,
|
|
146
|
+
scheme: @scheme
|
|
127
147
|
)
|
|
128
148
|
end
|
|
129
149
|
else
|
|
130
150
|
result_intervals << interval
|
|
131
151
|
end
|
|
132
152
|
end
|
|
133
|
-
|
|
134
|
-
self.class.new(result_intervals)
|
|
153
|
+
|
|
154
|
+
self.class.new(result_intervals, raw_constraints: raw_constraints, scheme: @scheme)
|
|
135
155
|
end
|
|
136
156
|
|
|
137
157
|
def to_s
|
|
@@ -165,9 +185,39 @@ module Vers
|
|
|
165
185
|
return 0 if a == b
|
|
166
186
|
return -1 if a.nil?
|
|
167
187
|
return 1 if b.nil?
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
|
|
189
|
+
if @scheme
|
|
190
|
+
Version.compare_with_scheme(a, b, @scheme)
|
|
191
|
+
else
|
|
192
|
+
Version.compare(a, b)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def compare_interval_bounds(a, b)
|
|
197
|
+
min_a = a.min
|
|
198
|
+
min_b = b.min
|
|
199
|
+
min_cmp = if min_a.nil? && min_b.nil?
|
|
200
|
+
0
|
|
201
|
+
elsif min_a.nil?
|
|
202
|
+
-1
|
|
203
|
+
elsif min_b.nil?
|
|
204
|
+
1
|
|
205
|
+
else
|
|
206
|
+
version_compare(min_a, min_b)
|
|
207
|
+
end
|
|
208
|
+
return min_cmp unless min_cmp == 0
|
|
209
|
+
|
|
210
|
+
max_a = a.max
|
|
211
|
+
max_b = b.max
|
|
212
|
+
if max_a.nil? && max_b.nil?
|
|
213
|
+
0
|
|
214
|
+
elsif max_a.nil?
|
|
215
|
+
1
|
|
216
|
+
elsif max_b.nil?
|
|
217
|
+
-1
|
|
218
|
+
else
|
|
219
|
+
version_compare(max_a, max_b)
|
|
220
|
+
end
|
|
171
221
|
end
|
|
172
222
|
end
|
|
173
223
|
end
|
data/lib/vers.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "vers/version"
|
|
4
|
+
require_relative "vers/maven_version"
|
|
5
|
+
require_relative "vers/nuget_version"
|
|
4
6
|
require_relative "vers/interval"
|
|
5
7
|
require_relative "vers/version_range"
|
|
6
8
|
require_relative "vers/constraint"
|
|
@@ -117,13 +119,15 @@ module Vers
|
|
|
117
119
|
# Vers.satisfies?("1.5.0", "^1.2.3", "npm") # => true
|
|
118
120
|
#
|
|
119
121
|
def self.satisfies?(version, constraint, scheme = nil)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
131
|
end
|
|
128
132
|
|
|
129
133
|
##
|
|
@@ -143,6 +147,18 @@ module Vers
|
|
|
143
147
|
Version.compare(a, b)
|
|
144
148
|
end
|
|
145
149
|
|
|
150
|
+
##
|
|
151
|
+
# Compares two version strings using scheme-specific rules
|
|
152
|
+
#
|
|
153
|
+
# @param a [String] First version string
|
|
154
|
+
# @param b [String] Second version string
|
|
155
|
+
# @param scheme [String, nil] Package manager scheme (maven, nuget, or nil for generic)
|
|
156
|
+
# @return [Integer] -1 if a < b, 0 if a == b, 1 if a > b
|
|
157
|
+
#
|
|
158
|
+
def self.compare_with_scheme(a, b, scheme)
|
|
159
|
+
Version.compare_with_scheme(a, b, scheme)
|
|
160
|
+
end
|
|
161
|
+
|
|
146
162
|
##
|
|
147
163
|
# Normalizes a version string to a consistent format
|
|
148
164
|
#
|
|
@@ -163,6 +179,10 @@ module Vers
|
|
|
163
179
|
Version.valid?(version_string)
|
|
164
180
|
end
|
|
165
181
|
|
|
182
|
+
def self.clean(version_string)
|
|
183
|
+
Version.clean(version_string)
|
|
184
|
+
end
|
|
185
|
+
|
|
166
186
|
##
|
|
167
187
|
# Creates an exact version range
|
|
168
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.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
|
|
@@ -30,6 +31,8 @@ files:
|
|
|
30
31
|
- lib/vers.rb
|
|
31
32
|
- lib/vers/constraint.rb
|
|
32
33
|
- lib/vers/interval.rb
|
|
34
|
+
- lib/vers/maven_version.rb
|
|
35
|
+
- lib/vers/nuget_version.rb
|
|
33
36
|
- lib/vers/parser.rb
|
|
34
37
|
- lib/vers/version.rb
|
|
35
38
|
- lib/vers/version_range.rb
|