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
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vers
|
4
|
+
##
|
5
|
+
# Represents a single version constraint (e.g., ">=1.2.3", "!=2.0.0")
|
6
|
+
#
|
7
|
+
# A constraint consists of an operator and a version. This class handles
|
8
|
+
# parsing constraint strings and converting them to intervals.
|
9
|
+
#
|
10
|
+
# == Examples
|
11
|
+
#
|
12
|
+
# constraint = Vers::Constraint.new(">=", "1.2.3")
|
13
|
+
# constraint.operator # => ">="
|
14
|
+
# constraint.version # => "1.2.3"
|
15
|
+
# constraint.to_interval # => [1.2.3,+∞)
|
16
|
+
#
|
17
|
+
class Constraint
|
18
|
+
# Valid constraint operators as defined in the vers spec
|
19
|
+
OPERATORS = %w[= != < <= > >=].freeze
|
20
|
+
|
21
|
+
attr_reader :operator, :version
|
22
|
+
|
23
|
+
##
|
24
|
+
# Creates a new constraint with the given operator and version
|
25
|
+
#
|
26
|
+
# @param operator [String] The constraint operator (=, !=, <, <=, >, >=)
|
27
|
+
# @param version [String] The version string
|
28
|
+
# @raise [ArgumentError] if operator is invalid
|
29
|
+
#
|
30
|
+
def initialize(operator, version)
|
31
|
+
raise ArgumentError, "Invalid operator: #{operator}" unless OPERATORS.include?(operator)
|
32
|
+
|
33
|
+
@operator = operator
|
34
|
+
@version = version
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Parses a constraint string into operator and version components
|
39
|
+
#
|
40
|
+
# @param constraint_string [String] The constraint string to parse
|
41
|
+
# @return [Constraint] A new constraint object
|
42
|
+
# @raise [ArgumentError] if the constraint string is invalid
|
43
|
+
#
|
44
|
+
# == Examples
|
45
|
+
#
|
46
|
+
# Vers::Constraint.parse(">=1.2.3") # => #<Vers::Constraint:0x... @operator=">=", @version="1.2.3">
|
47
|
+
# Vers::Constraint.parse("!=2.0.0") # => #<Vers::Constraint:0x... @operator="!=", @version="2.0.0">
|
48
|
+
#
|
49
|
+
def self.parse(constraint_string)
|
50
|
+
return new("=", constraint_string) unless constraint_string.match(/^[!<>=]/)
|
51
|
+
|
52
|
+
if constraint_string.start_with?("!=")
|
53
|
+
version = constraint_string[2..-1]
|
54
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}" if version.empty?
|
55
|
+
new("!=", version)
|
56
|
+
elsif constraint_string.start_with?(">=")
|
57
|
+
version = constraint_string[2..-1]
|
58
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}" if version.empty?
|
59
|
+
new(">=", version)
|
60
|
+
elsif constraint_string.start_with?("<=")
|
61
|
+
version = constraint_string[2..-1]
|
62
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}" if version.empty?
|
63
|
+
new("<=", version)
|
64
|
+
elsif constraint_string.start_with?(">")
|
65
|
+
version = constraint_string[1..-1]
|
66
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}" if version.empty?
|
67
|
+
new(">", version)
|
68
|
+
elsif constraint_string.start_with?("<")
|
69
|
+
version = constraint_string[1..-1]
|
70
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}" if version.empty?
|
71
|
+
new("<", version)
|
72
|
+
elsif constraint_string.start_with?("=")
|
73
|
+
version = constraint_string[1..-1]
|
74
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}" if version.empty?
|
75
|
+
new("=", version)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "Invalid constraint format: #{constraint_string}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Converts this constraint to an interval representation
|
83
|
+
#
|
84
|
+
# @return [Interval] The interval representation of this constraint
|
85
|
+
#
|
86
|
+
# == Examples
|
87
|
+
#
|
88
|
+
# Vers::Constraint.new(">=", "1.2.3").to_interval # => [1.2.3,+∞)
|
89
|
+
# Vers::Constraint.new("=", "1.0.0").to_interval # => [1.0.0,1.0.0]
|
90
|
+
#
|
91
|
+
def to_interval
|
92
|
+
case operator
|
93
|
+
when "="
|
94
|
+
Interval.exact(version)
|
95
|
+
when "!="
|
96
|
+
# != constraints need special handling in ranges - they create exclusions
|
97
|
+
nil
|
98
|
+
when ">"
|
99
|
+
Interval.greater_than(version, inclusive: false)
|
100
|
+
when ">="
|
101
|
+
Interval.greater_than(version, inclusive: true)
|
102
|
+
when "<"
|
103
|
+
Interval.less_than(version, inclusive: false)
|
104
|
+
when "<="
|
105
|
+
Interval.less_than(version, inclusive: true)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Returns true if this is an exclusion constraint (!=)
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
113
|
+
#
|
114
|
+
def exclusion?
|
115
|
+
operator == "!="
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Checks if a version satisfies this constraint
|
120
|
+
#
|
121
|
+
# @param version_string [String] The version to check
|
122
|
+
# @return [Boolean] true if the version satisfies the constraint
|
123
|
+
#
|
124
|
+
def satisfies?(version_string)
|
125
|
+
case operator
|
126
|
+
when "="
|
127
|
+
Version.compare(version_string, version) == 0
|
128
|
+
when "!="
|
129
|
+
Version.compare(version_string, version) != 0
|
130
|
+
when ">"
|
131
|
+
Version.compare(version_string, version) > 0
|
132
|
+
when ">="
|
133
|
+
Version.compare(version_string, version) >= 0
|
134
|
+
when "<"
|
135
|
+
Version.compare(version_string, version) < 0
|
136
|
+
when "<="
|
137
|
+
Version.compare(version_string, version) <= 0
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# String representation of this constraint
|
143
|
+
#
|
144
|
+
# @return [String] The constraint as a string
|
145
|
+
#
|
146
|
+
def to_s
|
147
|
+
"#{operator}#{version}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def ==(other)
|
151
|
+
other.is_a?(Constraint) && operator == other.operator && version == other.version
|
152
|
+
end
|
153
|
+
|
154
|
+
def hash
|
155
|
+
[operator, version].hash
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'version'
|
4
|
+
|
5
|
+
module Vers
|
6
|
+
class Interval
|
7
|
+
attr_reader :min, :max, :min_inclusive, :max_inclusive
|
8
|
+
|
9
|
+
def initialize(min: nil, max: nil, min_inclusive: true, max_inclusive: true)
|
10
|
+
@min = min
|
11
|
+
@max = max
|
12
|
+
@min_inclusive = min_inclusive
|
13
|
+
@max_inclusive = max_inclusive
|
14
|
+
|
15
|
+
validate_bounds!
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.empty
|
19
|
+
new(min: "1", max: "0", min_inclusive: true, max_inclusive: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.unbounded
|
23
|
+
new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.exact(version)
|
27
|
+
new(min: version, max: version, min_inclusive: true, max_inclusive: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.greater_than(version, inclusive: false)
|
31
|
+
new(min: version, min_inclusive: inclusive)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.less_than(version, inclusive: false)
|
35
|
+
new(max: version, max_inclusive: inclusive)
|
36
|
+
end
|
37
|
+
|
38
|
+
def empty?
|
39
|
+
return true if min && max && version_compare(min, max) > 0
|
40
|
+
return true if min && max && version_compare(min, max) == 0 && (!min_inclusive || !max_inclusive)
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def unbounded?
|
45
|
+
min.nil? && max.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def contains?(version)
|
49
|
+
return false if empty?
|
50
|
+
return true if unbounded?
|
51
|
+
|
52
|
+
within_min = min.nil? ||
|
53
|
+
(min_inclusive ? version_compare(version, min) >= 0 : version_compare(version, min) > 0)
|
54
|
+
|
55
|
+
within_max = max.nil? ||
|
56
|
+
(max_inclusive ? version_compare(version, max) <= 0 : version_compare(version, max) < 0)
|
57
|
+
|
58
|
+
within_min && within_max
|
59
|
+
end
|
60
|
+
|
61
|
+
def intersect(other)
|
62
|
+
return self.class.empty if empty? || other.empty?
|
63
|
+
|
64
|
+
new_min = nil
|
65
|
+
new_min_inclusive = true
|
66
|
+
new_max = nil
|
67
|
+
new_max_inclusive = true
|
68
|
+
|
69
|
+
if min && other.min
|
70
|
+
comparison = version_compare(min, other.min)
|
71
|
+
if comparison > 0
|
72
|
+
new_min = min
|
73
|
+
new_min_inclusive = min_inclusive
|
74
|
+
elsif comparison < 0
|
75
|
+
new_min = other.min
|
76
|
+
new_min_inclusive = other.min_inclusive
|
77
|
+
else
|
78
|
+
new_min = min
|
79
|
+
new_min_inclusive = min_inclusive && other.min_inclusive
|
80
|
+
end
|
81
|
+
elsif min
|
82
|
+
new_min = min
|
83
|
+
new_min_inclusive = min_inclusive
|
84
|
+
elsif other.min
|
85
|
+
new_min = other.min
|
86
|
+
new_min_inclusive = other.min_inclusive
|
87
|
+
end
|
88
|
+
|
89
|
+
if max && other.max
|
90
|
+
comparison = version_compare(max, other.max)
|
91
|
+
if comparison < 0
|
92
|
+
new_max = max
|
93
|
+
new_max_inclusive = max_inclusive
|
94
|
+
elsif comparison > 0
|
95
|
+
new_max = other.max
|
96
|
+
new_max_inclusive = other.max_inclusive
|
97
|
+
else
|
98
|
+
new_max = max
|
99
|
+
new_max_inclusive = max_inclusive && other.max_inclusive
|
100
|
+
end
|
101
|
+
elsif max
|
102
|
+
new_max = max
|
103
|
+
new_max_inclusive = max_inclusive
|
104
|
+
elsif other.max
|
105
|
+
new_max = other.max
|
106
|
+
new_max_inclusive = other.max_inclusive
|
107
|
+
end
|
108
|
+
|
109
|
+
self.class.new(
|
110
|
+
min: new_min,
|
111
|
+
max: new_max,
|
112
|
+
min_inclusive: new_min_inclusive,
|
113
|
+
max_inclusive: new_max_inclusive
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
def union(other)
|
118
|
+
return other if empty?
|
119
|
+
return self if other.empty?
|
120
|
+
|
121
|
+
return nil unless overlaps?(other) || adjacent?(other)
|
122
|
+
|
123
|
+
new_min = nil
|
124
|
+
new_min_inclusive = true
|
125
|
+
new_max = nil
|
126
|
+
new_max_inclusive = true
|
127
|
+
|
128
|
+
if min && other.min
|
129
|
+
comparison = version_compare(min, other.min)
|
130
|
+
if comparison < 0
|
131
|
+
new_min = min
|
132
|
+
new_min_inclusive = min_inclusive
|
133
|
+
elsif comparison > 0
|
134
|
+
new_min = other.min
|
135
|
+
new_min_inclusive = other.min_inclusive
|
136
|
+
else
|
137
|
+
new_min = min
|
138
|
+
new_min_inclusive = min_inclusive || other.min_inclusive
|
139
|
+
end
|
140
|
+
elsif min.nil?
|
141
|
+
new_min = other.min
|
142
|
+
new_min_inclusive = other.min_inclusive
|
143
|
+
elsif other.min.nil?
|
144
|
+
new_min = min
|
145
|
+
new_min_inclusive = min_inclusive
|
146
|
+
end
|
147
|
+
|
148
|
+
if max && other.max
|
149
|
+
comparison = version_compare(max, other.max)
|
150
|
+
if comparison > 0
|
151
|
+
new_max = max
|
152
|
+
new_max_inclusive = max_inclusive
|
153
|
+
elsif comparison < 0
|
154
|
+
new_max = other.max
|
155
|
+
new_max_inclusive = other.max_inclusive
|
156
|
+
else
|
157
|
+
new_max = max
|
158
|
+
new_max_inclusive = max_inclusive || other.max_inclusive
|
159
|
+
end
|
160
|
+
elsif max.nil?
|
161
|
+
new_max = other.max
|
162
|
+
new_max_inclusive = other.max_inclusive
|
163
|
+
elsif other.max.nil?
|
164
|
+
new_max = max
|
165
|
+
new_max_inclusive = max_inclusive
|
166
|
+
end
|
167
|
+
|
168
|
+
self.class.new(
|
169
|
+
min: new_min,
|
170
|
+
max: new_max,
|
171
|
+
min_inclusive: new_min_inclusive,
|
172
|
+
max_inclusive: new_max_inclusive
|
173
|
+
)
|
174
|
+
end
|
175
|
+
|
176
|
+
def overlaps?(other)
|
177
|
+
return false if empty? || other.empty?
|
178
|
+
!intersect(other).empty?
|
179
|
+
end
|
180
|
+
|
181
|
+
def adjacent?(other)
|
182
|
+
return false if empty? || other.empty?
|
183
|
+
|
184
|
+
if max && other.min && version_compare(max, other.min) == 0
|
185
|
+
return (max_inclusive && !other.min_inclusive) || (!max_inclusive && other.min_inclusive)
|
186
|
+
end
|
187
|
+
|
188
|
+
if min && other.max && version_compare(min, other.max) == 0
|
189
|
+
return (min_inclusive && !other.max_inclusive) || (!min_inclusive && other.max_inclusive)
|
190
|
+
end
|
191
|
+
|
192
|
+
false
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_s
|
196
|
+
return "∅" if empty?
|
197
|
+
return "(-∞,+∞)" if unbounded?
|
198
|
+
|
199
|
+
min_bracket = min_inclusive ? "[" : "("
|
200
|
+
max_bracket = max_inclusive ? "]" : ")"
|
201
|
+
min_str = min || "-∞"
|
202
|
+
max_str = max || "+∞"
|
203
|
+
|
204
|
+
"#{min_bracket}#{min_str},#{max_str}#{max_bracket}"
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def validate_bounds!
|
210
|
+
return unless min && max
|
211
|
+
|
212
|
+
comparison = version_compare(min, max)
|
213
|
+
if comparison > 0
|
214
|
+
return
|
215
|
+
elsif comparison == 0 && (!min_inclusive || !max_inclusive)
|
216
|
+
return
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def version_compare(a, b)
|
221
|
+
return 0 if a == b
|
222
|
+
return -1 if a.nil?
|
223
|
+
return 1 if b.nil?
|
224
|
+
|
225
|
+
# Use the Version class for comparison
|
226
|
+
Version.compare(a, b)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|