vers 1.3.1 → 2.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,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vers
4
+ module PubVersion
5
+ extend self
6
+
7
+ PATTERN = /\A[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z/
8
+
9
+ def compare(left, right)
10
+ parsed_left = parse(left)
11
+ parsed_right = parse(right)
12
+ return Version.compare(left, right) unless parsed_left && parsed_right
13
+
14
+ parsed_left.core.zip(parsed_right.core).each do |left_part, right_part|
15
+ comparison = VersionComparison.compare_numbers(left_part, right_part)
16
+ return comparison unless comparison.zero?
17
+ end
18
+
19
+ comparison = SemverVersion.compare_prerelease(parsed_left.prerelease, parsed_right.prerelease)
20
+ return comparison unless comparison.zero?
21
+
22
+ compare_build(parsed_left.build, parsed_right.build)
23
+ end
24
+
25
+ def parse(value)
26
+ return nil unless PATTERN.match?(value.to_s)
27
+
28
+ SemverVersion.parse(value)
29
+ end
30
+
31
+ def valid?(value)
32
+ !parse(value.to_s.strip).nil?
33
+ end
34
+
35
+ def normalize(value)
36
+ normalized = SemverVersion.normalize(value.to_s.strip)
37
+ core_and_prerelease, build = normalized.split("+", 2)
38
+ return core_and_prerelease unless build
39
+
40
+ normalized_build = build.split(".").map do |part|
41
+ VersionComparison.numeric?(part) ? VersionComparison.normalize_number(part) : part
42
+ end
43
+ "#{core_and_prerelease}+#{normalized_build.join(".")}"
44
+ end
45
+
46
+ def prerelease?(value)
47
+ parsed = parse(value.to_s.strip)
48
+ parsed && !parsed.prerelease.empty?
49
+ end
50
+
51
+ def compare_build(left, right)
52
+ return 0 if left.empty? && right.empty?
53
+ return -1 if left.empty?
54
+ return 1 if right.empty?
55
+
56
+ SemverVersion.compare_prerelease(left, right)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version_comparison"
4
+
5
+ module Vers
6
+ module PyPIVersion
7
+ extend self
8
+
9
+ PATTERN = /\A\s*v?
10
+ (?:(\d+)!)?
11
+ (\d+(?:\.\d+)*)
12
+ (?:[-_.]?(alpha|beta|preview|pre|rc|a|b|c)[-_.]?(\d*))?
13
+ (?:(?:[-_.]?(post|rev|r)[-_.]?(\d*))|(?:-(\d+)))?
14
+ (?:[-_.]?(dev)[-_.]?(\d*))?
15
+ (?:\+([a-z0-9]+(?:[-_.][a-z0-9]+)*))?
16
+ \s*\z/ix
17
+
18
+ PRE_TAGS = {
19
+ "a" => 0,
20
+ "alpha" => 0,
21
+ "b" => 1,
22
+ "beta" => 1,
23
+ "c" => 2,
24
+ "rc" => 2,
25
+ "pre" => 2,
26
+ "preview" => 2
27
+ }.freeze
28
+
29
+ LocalPart = Data.define(:value, :numeric)
30
+ Parsed = Data.define(
31
+ :epoch,
32
+ :release,
33
+ :pre_tag,
34
+ :pre_number,
35
+ :post_number,
36
+ :dev_number,
37
+ :local
38
+ )
39
+
40
+ def compare(left, right)
41
+ parsed_left = parse(left)
42
+ parsed_right = parse(right)
43
+ comparison = compare_public_parsed(left, right, parsed_left, parsed_right)
44
+ return comparison unless comparison.zero?
45
+ return comparison unless parsed_left && parsed_right
46
+
47
+ compare_local(parsed_left.local, parsed_right.local)
48
+ end
49
+
50
+ def compare_public(left, right)
51
+ parsed_left = parse(left)
52
+ parsed_right = parse(right)
53
+ compare_public_parsed(left, right, parsed_left, parsed_right)
54
+ end
55
+
56
+ def compare_public_parsed(left, right, parsed_left, parsed_right)
57
+ return Version.compare(left, right) unless parsed_left && parsed_right
58
+
59
+ comparison = VersionComparison.compare_numbers(parsed_left.epoch, parsed_right.epoch)
60
+ return comparison unless comparison.zero?
61
+
62
+ comparison = compare_release(parsed_left.release, parsed_right.release)
63
+ return comparison unless comparison.zero?
64
+
65
+ comparison = compare_pre(parsed_left, parsed_right)
66
+ return comparison unless comparison.zero?
67
+
68
+ comparison = compare_post(parsed_left, parsed_right)
69
+ return comparison unless comparison.zero?
70
+
71
+ comparison = compare_dev(parsed_left, parsed_right)
72
+ return comparison unless comparison.zero?
73
+
74
+ 0
75
+ end
76
+
77
+ def parse(value)
78
+ match = PATTERN.match(value.to_s)
79
+ return nil unless match
80
+
81
+ pre_tag = match[3]&.downcase
82
+ post_number = match[7] || (match[5] ? match[6] : nil)
83
+ local = if match[10]
84
+ match[10].downcase.split(/[._-]/).map do |part|
85
+ LocalPart.new(part, VersionComparison.numeric?(part))
86
+ end
87
+ else
88
+ []
89
+ end
90
+
91
+ Parsed.new(
92
+ match[1].to_s,
93
+ match[2].split("."),
94
+ pre_tag && PRE_TAGS.fetch(pre_tag),
95
+ pre_tag ? match[4].to_s : nil,
96
+ post_number,
97
+ match[8] ? match[9].to_s : nil,
98
+ local.freeze
99
+ )
100
+ end
101
+
102
+ def valid?(value)
103
+ !parse(value).nil?
104
+ end
105
+
106
+ def prerelease?(value)
107
+ parsed = parse(value)
108
+ parsed && (!parsed.pre_tag.nil? || !parsed.dev_number.nil?)
109
+ end
110
+
111
+ def stable?(value)
112
+ parsed = parse(value)
113
+ parsed && parsed.pre_tag.nil? && parsed.dev_number.nil?
114
+ end
115
+
116
+ def normalize(value)
117
+ parsed = parse(value)
118
+ raise ArgumentError, "invalid pypi version: #{value}" unless parsed
119
+
120
+ result = +""
121
+ result << "#{VersionComparison.normalize_number(parsed.epoch)}!" unless VersionComparison.compare_numbers(parsed.epoch, "0").zero?
122
+ result << parsed.release.map { |part| VersionComparison.normalize_number(part) }.join(".")
123
+
124
+ unless parsed.pre_tag.nil?
125
+ result << %w[a b rc].fetch(parsed.pre_tag)
126
+ result << VersionComparison.normalize_number(parsed.pre_number)
127
+ end
128
+ result << ".post#{VersionComparison.normalize_number(parsed.post_number)}" unless parsed.post_number.nil?
129
+ result << ".dev#{VersionComparison.normalize_number(parsed.dev_number)}" unless parsed.dev_number.nil?
130
+
131
+ if parsed.local.any?
132
+ result << "+"
133
+ result << parsed.local.map { |part| part.numeric ? VersionComparison.normalize_number(part.value) : part.value }.join(".")
134
+ end
135
+
136
+ result
137
+ end
138
+
139
+ def specifier_equal?(version, specifier)
140
+ parsed_version = parse(version)
141
+ parsed_specifier = parse(specifier)
142
+ return compare(version, specifier).zero? unless parsed_version && parsed_specifier
143
+ versions_equal?(parsed_version, parsed_specifier, ignore_local: parsed_specifier.local.empty?)
144
+ end
145
+
146
+ def versions_equal?(left, right, ignore_local: false)
147
+ local_equal = ignore_local || compare_local(left.local, right.local).zero?
148
+ VersionComparison.compare_numbers(left.epoch, right.epoch).zero? &&
149
+ compare_release(left.release, right.release).zero? &&
150
+ compare_pre(left, right).zero? &&
151
+ compare_post(left, right).zero? &&
152
+ compare_dev(left, right).zero? &&
153
+ local_equal
154
+ end
155
+
156
+ def same_release?(left, right)
157
+ VersionComparison.compare_numbers(left.epoch, right.epoch).zero? &&
158
+ compare_release(left.release, right.release).zero?
159
+ end
160
+
161
+ def without_post_and_dev(version)
162
+ Parsed.new(
163
+ version.epoch,
164
+ version.release,
165
+ version.pre_tag,
166
+ version.pre_number,
167
+ nil,
168
+ nil,
169
+ [].freeze
170
+ )
171
+ end
172
+
173
+ def without_dev(version)
174
+ Parsed.new(
175
+ version.epoch,
176
+ version.release,
177
+ version.pre_tag,
178
+ version.pre_number,
179
+ version.post_number,
180
+ nil,
181
+ [].freeze
182
+ )
183
+ end
184
+
185
+ def compare_release(left, right)
186
+ [left.length, right.length].max.times do |index|
187
+ comparison = VersionComparison.compare_numbers(left[index], right[index])
188
+ return comparison unless comparison.zero?
189
+ end
190
+
191
+ 0
192
+ end
193
+
194
+ def compare_pre(left, right)
195
+ left_rank = pre_rank(left)
196
+ right_rank = pre_rank(right)
197
+ comparison = left_rank <=> right_rank
198
+ return comparison unless comparison.zero?
199
+ return 0 if left.pre_tag.nil?
200
+
201
+ comparison = left.pre_tag <=> right.pre_tag
202
+ return comparison unless comparison.zero?
203
+
204
+ VersionComparison.compare_numbers(left.pre_number, right.pre_number)
205
+ end
206
+
207
+ def pre_rank(version)
208
+ return -1 if version.pre_tag.nil? && version.post_number.nil? && !version.dev_number.nil?
209
+ return 1 if version.pre_tag.nil?
210
+
211
+ 0
212
+ end
213
+
214
+ def compare_post(left, right)
215
+ return 1 if !left.post_number.nil? && right.post_number.nil?
216
+ return -1 if left.post_number.nil? && !right.post_number.nil?
217
+ return 0 if left.post_number.nil?
218
+
219
+ VersionComparison.compare_numbers(left.post_number, right.post_number)
220
+ end
221
+
222
+ def compare_dev(left, right)
223
+ return -1 if !left.dev_number.nil? && right.dev_number.nil?
224
+ return 1 if left.dev_number.nil? && !right.dev_number.nil?
225
+ return 0 if left.dev_number.nil?
226
+
227
+ VersionComparison.compare_numbers(left.dev_number, right.dev_number)
228
+ end
229
+
230
+ def compare_local(left, right)
231
+ return 0 if left.empty? && right.empty?
232
+ return -1 if left.empty?
233
+ return 1 if right.empty?
234
+
235
+ [left.length, right.length].max.times do |index|
236
+ return -1 unless left[index]
237
+ return 1 unless right[index]
238
+
239
+ left_part = left[index]
240
+ right_part = right[index]
241
+ if left_part.numeric != right_part.numeric
242
+ return left_part.numeric ? 1 : -1
243
+ end
244
+
245
+ comparison = if left_part.numeric
246
+ VersionComparison.compare_numbers(left_part.value, right_part.value)
247
+ else
248
+ left_part.value <=> right_part.value
249
+ end
250
+ return comparison unless comparison.zero?
251
+ end
252
+
253
+ 0
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vers
4
+ module Scheme
5
+ extend self
6
+
7
+ ALIASES = {
8
+ "rubygems" => "gem",
9
+ "debian" => "deb",
10
+ "golang" => "go",
11
+ "elixir" => "hex",
12
+ "alpine" => "apk"
13
+ }.freeze
14
+
15
+ HANDLERS = {
16
+ "alpm" => ALPMVersion,
17
+ "apk" => APKVersion,
18
+ "bazel" => BazelVersion,
19
+ "cargo" => CargoVersion,
20
+ "composer" => ComposerVersion,
21
+ "conan" => ConanVersion,
22
+ "deb" => DebianVersion,
23
+ "datetime" => DatetimeVersion,
24
+ "gem" => GemVersion,
25
+ "gentoo" => GentooVersion,
26
+ "go" => GoVersion,
27
+ "hex" => SemverVersion,
28
+ "intdot" => IntDotVersion,
29
+ "lexicographic" => LexicographicVersion,
30
+ "maven" => MavenVersion,
31
+ "nginx" => SemverVersion,
32
+ "npm" => NpmVersion,
33
+ "nuget" => NuGetVersion,
34
+ "openssl" => OpenSSLVersion,
35
+ "pub" => PubVersion,
36
+ "pypi" => PyPIVersion,
37
+ "rpm" => RPMVersion,
38
+ "semver" => SemverVersion
39
+ }.freeze
40
+
41
+ def canonical(value)
42
+ return nil if value.nil?
43
+
44
+ scheme = value.to_s.downcase
45
+ ALIASES.fetch(scheme, scheme)
46
+ end
47
+
48
+ def handler(value)
49
+ HANDLERS[canonical(value)]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version_comparison"
4
+
5
+ module Vers
6
+ module SemverVersion
7
+ extend self
8
+
9
+ Parsed = Data.define(:core, :prerelease, :build)
10
+
11
+ def compare(left, right)
12
+ parsed_left = parse(left)
13
+ parsed_right = parse(right)
14
+ return left <=> right unless parsed_left && parsed_right
15
+
16
+ parsed_left.core.zip(parsed_right.core).each do |left_part, right_part|
17
+ comparison = VersionComparison.compare_numbers(left_part, right_part)
18
+ return comparison unless comparison.zero?
19
+ end
20
+
21
+ compare_prerelease(parsed_left.prerelease, parsed_right.prerelease)
22
+ end
23
+
24
+ def parse(value)
25
+ string = value.to_s.strip
26
+ index = string.start_with?("v") ? 1 : 0
27
+ core = []
28
+
29
+ 3.times do |part|
30
+ start = index
31
+ index += 1 while index < string.length && string.getbyte(index).between?(48, 57)
32
+ return nil if index == start
33
+
34
+ core << string[start...index]
35
+ break unless index < string.length && string.getbyte(index) == 46
36
+ return nil if part == 2
37
+
38
+ index += 1
39
+ end
40
+
41
+ core << "0" while core.length < 3
42
+ prerelease = ""
43
+ build = ""
44
+
45
+ if index < string.length && string.getbyte(index) == 45
46
+ start = index + 1
47
+ index = start
48
+ index += 1 while index < string.length && string.getbyte(index) != 43
49
+ return nil if index == start
50
+
51
+ prerelease = string[start...index]
52
+ end
53
+
54
+ if index < string.length && string.getbyte(index) == 43
55
+ index += 1
56
+ return nil if index == string.length || string[index..].include?("\n")
57
+
58
+ build = string[index..]
59
+ index = string.length
60
+ end
61
+
62
+ return nil unless index == string.length
63
+
64
+ Parsed.new(core.freeze, prerelease, build)
65
+ end
66
+
67
+ def valid?(value)
68
+ parsed = parse(value.to_s.strip)
69
+ return false unless parsed
70
+
71
+ [parsed.prerelease, parsed.build].all? do |field|
72
+ field.empty? || field.split(".", -1).all? { |part| part.match?(/\A[0-9A-Za-z-]+\z/) }
73
+ end
74
+ end
75
+
76
+ def normalize(value, preserve_v: false)
77
+ string = value.to_s.strip
78
+ parsed = parse(string)
79
+ raise ArgumentError, "Invalid semantic version: #{value}" unless parsed && valid?(string)
80
+
81
+ result = parsed.core.map { |part| VersionComparison.normalize_number(part) }.join(".")
82
+ result = "v#{result}" if preserve_v && string.start_with?("v")
83
+ unless parsed.prerelease.empty?
84
+ prerelease = parsed.prerelease.split(".").map do |part|
85
+ VersionComparison.numeric?(part) ? VersionComparison.normalize_number(part) : part
86
+ end
87
+ result += "-#{prerelease.join(".")}"
88
+ end
89
+ result += "+#{parsed.build}" unless parsed.build.empty?
90
+ result
91
+ end
92
+
93
+ def prerelease?(value)
94
+ parsed = parse(value.to_s.strip)
95
+ parsed && !parsed.prerelease.empty?
96
+ end
97
+
98
+ def compare_prerelease(left, right)
99
+ return 0 if left.empty? && right.empty?
100
+ return 1 if left.empty?
101
+ return -1 if right.empty?
102
+
103
+ left_parts = left.split(".", -1)
104
+ right_parts = right.split(".", -1)
105
+ left_parts.zip(right_parts).each do |left_part, right_part|
106
+ return 1 if right_part.nil?
107
+
108
+ comparison = compare_identifier(left_part, right_part)
109
+ return comparison unless comparison.zero?
110
+ end
111
+
112
+ left_parts.length <=> right_parts.length
113
+ end
114
+
115
+ def compare_identifier(left, right)
116
+ left_numeric = VersionComparison.numeric?(left)
117
+ right_numeric = VersionComparison.numeric?(right)
118
+ return -1 if left_numeric && !right_numeric
119
+ return 1 if !left_numeric && right_numeric
120
+
121
+ if left_numeric
122
+ VersionComparison.compare_numbers(left, right)
123
+ else
124
+ left <=> right
125
+ end
126
+ end
127
+ end
128
+
129
+ module NpmVersion
130
+ extend self
131
+
132
+ def compare(left, right)
133
+ SemverVersion.compare(left.strip, right.strip)
134
+ end
135
+
136
+ def valid?(value)
137
+ SemverVersion.valid?(value.to_s.strip)
138
+ end
139
+
140
+ def normalize(value)
141
+ SemverVersion.normalize(value.to_s.strip)
142
+ end
143
+
144
+ def prerelease?(value)
145
+ SemverVersion.prerelease?(value.to_s.strip)
146
+ end
147
+ end
148
+
149
+ module CargoVersion
150
+ extend self
151
+
152
+ def compare(left, right)
153
+ comparison = SemverVersion.compare(left, right)
154
+ return comparison unless comparison.zero?
155
+
156
+ parsed_left = SemverVersion.parse(left)
157
+ parsed_right = SemverVersion.parse(right)
158
+ return comparison unless parsed_left && parsed_right
159
+
160
+ compare_build(parsed_left.build, parsed_right.build)
161
+ end
162
+
163
+ def compare_build(left, right)
164
+ return 0 if left == right
165
+ return -1 if left.empty?
166
+ return 1 if right.empty?
167
+
168
+ left_parts = left.split(".", -1)
169
+ right_parts = right.split(".", -1)
170
+ left_parts.zip(right_parts).each do |left_part, right_part|
171
+ return 1 if right_part.nil?
172
+
173
+ comparison = compare_build_identifier(left_part, right_part)
174
+ return comparison unless comparison.zero?
175
+ end
176
+
177
+ left_parts.length <=> right_parts.length
178
+ end
179
+
180
+ def compare_build_identifier(left, right)
181
+ left_numeric = VersionComparison.numeric?(left)
182
+ right_numeric = VersionComparison.numeric?(right)
183
+ return -1 if left_numeric && !right_numeric
184
+ return 1 if !left_numeric && right_numeric
185
+ return left <=> right unless left_numeric
186
+
187
+ normalized_left = VersionComparison.normalize_number(left)
188
+ normalized_right = VersionComparison.normalize_number(right)
189
+ comparison = normalized_left.length <=> normalized_right.length
190
+ return comparison unless comparison.zero?
191
+
192
+ comparison = normalized_left <=> normalized_right
193
+ return comparison unless comparison.zero?
194
+
195
+ left.length <=> right.length
196
+ end
197
+
198
+ def valid?(value)
199
+ SemverVersion.valid?(value.to_s.strip)
200
+ end
201
+
202
+ def normalize(value)
203
+ SemverVersion.normalize(value.to_s.strip)
204
+ end
205
+
206
+ def prerelease?(value)
207
+ SemverVersion.prerelease?(value.to_s.strip)
208
+ end
209
+ end
210
+
211
+ module GoVersion
212
+ extend self
213
+
214
+ def compare(left, right)
215
+ return SemverVersion.compare(left, right) unless left.start_with?("v") || right.start_with?("v")
216
+
217
+ parsed_left = parse(left)
218
+ parsed_right = parse(right)
219
+ return 1 if parsed_left && !parsed_right
220
+ return -1 if !parsed_left && parsed_right
221
+ return 0 unless parsed_left && parsed_right
222
+
223
+ parsed_left.core.zip(parsed_right.core).each do |left_part, right_part|
224
+ comparison = VersionComparison.compare_numbers(left_part, right_part)
225
+ return comparison unless comparison.zero?
226
+ end
227
+
228
+ SemverVersion.compare_prerelease(parsed_left.prerelease, parsed_right.prerelease)
229
+ end
230
+
231
+ def parse(value)
232
+ return nil unless value.start_with?("v")
233
+
234
+ string = value.delete_prefix("v")
235
+ core_end = string.index(/[-+]/) || string.length
236
+ core = string[0...core_end].split(".", -1)
237
+ return nil if core.empty? || core.length > 3
238
+ return nil unless core.all? { |part| valid_core_part?(part) }
239
+ return nil if core.length < 3 && core_end != string.length
240
+
241
+ core << "0" while core.length < 3
242
+ remainder = string[core_end..]
243
+ prerelease = ""
244
+ build = ""
245
+
246
+ if remainder.start_with?("-")
247
+ value = remainder.delete_prefix("-")
248
+ prerelease_end = value.index("+") || value.length
249
+ prerelease = value[0...prerelease_end]
250
+ return nil unless valid_identifiers?(prerelease, build: false)
251
+
252
+ remainder = value[prerelease_end..]
253
+ end
254
+
255
+ if remainder.start_with?("+")
256
+ build = remainder.delete_prefix("+")
257
+ return nil unless valid_identifiers?(build, build: true)
258
+
259
+ remainder = ""
260
+ end
261
+
262
+ return nil unless remainder.empty?
263
+
264
+ SemverVersion::Parsed.new(core.freeze, prerelease, build)
265
+ end
266
+
267
+ def valid_core_part?(part)
268
+ VersionComparison.numeric?(part) && (part == "0" || !part.start_with?("0"))
269
+ end
270
+
271
+ def valid_identifiers?(value, build:)
272
+ return false if value.empty?
273
+
274
+ value.split(".", -1).all? do |identifier|
275
+ next false unless identifier.match?(/\A[0-9A-Za-z-]+\z/)
276
+
277
+ build || identifier == "0" || !VersionComparison.numeric?(identifier) || !identifier.start_with?("0")
278
+ end
279
+ end
280
+
281
+ def valid?(value)
282
+ string = value.to_s.strip
283
+ string.start_with?("v") ? !parse(string).nil? : SemverVersion.valid?(string)
284
+ end
285
+
286
+ def normalize(value)
287
+ SemverVersion.normalize(value.to_s.strip, preserve_v: true)
288
+ end
289
+
290
+ def prerelease?(value)
291
+ SemverVersion.prerelease?(value.to_s.strip)
292
+ end
293
+ end
294
+ end