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.
@@ -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