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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a8066da910f228939dadbc96afeadcf5122c99ed9a06a67b9858c4720d7e070
4
- data.tar.gz: 59204f511b9adfb7863d97f754915fe96725093e6f0fa4dc7bfc3f2703e5e387
3
+ metadata.gz: e93f013566ede55047ab7d9ba9b328521e89f9eae2b9e5a21c589e6c3319ca45
4
+ data.tar.gz: 56e0c2ed53178935ed6548f0bc373a11343d2aa2d3de43bc51b888979c436578
5
5
  SHA512:
6
- metadata.gz: e4af64390c9efd167082d891eb192f051a04e231a889e751a646e7ce93d55d5736c5f98368ca1b83b6e4adaebc7f962ce4282155eabc245c522ed142a63dda27
7
- data.tar.gz: 119edf96cc65e3fc88d2f89b543ecbae593c6afeb136db0144793c981999476925ae5c671508965676b4d31d2a2cf1d4858feaaab45f443110561a5b74c00b06
6
+ metadata.gz: 30a8cb87c48bdb38480156560b03abcb16b9e3c1a4ff1a41726b824c7652b75782a3a6cfe2184290ed066281dcb8f478c0eb40150394e82b054314f39d4bdb86
7
+ data.tar.gz: f0ed8e70c9831e71fb17f3b058c2ab5b54d449352f0ec985f0c1c53962ef504e8f3d945278ad7c37af8847338078394bc9bb7e0d0317221486d1d501378bdf09
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.0
1
+ 4.0.6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.0] - 2026-08-28
11
+
12
+ ### Added
13
+
14
+ - Bazel module version comparison, validation, normalization, cleaning, stable classification, and prerelease classification.
15
+ - Scheme-aware comparison, validation, normalization, cleaning, stable classification, and prerelease classification across language registries, distribution packages, SemVer variants, and other version formats.
16
+ - Native range parsing for Composer, Pub, Conan, OpenSSL, and nginx, with expanded behavior for existing npm, Cargo, RubyGems, PyPI, Maven, and NuGet parsers.
17
+ - Data-driven comparison, containment, native conversion, validation, and canonical parsing tests from `vers-spec`, upstream package managers, and the Go implementation.
18
+
19
+ ### Changed
20
+
21
+ - Version ranges retain their canonical scheme through containment and set operations, and reject algebra between incompatible schemes.
22
+ - VERS serialization canonicalizes scheme aliases and rejects a scheme that conflicts with the range.
23
+ - Canonical VERS parsing rejects uppercase types, whitespace, malformed pipes, invalid percent encoding, unencoded reserved characters, duplicate versions, and unsorted constraints when strict parsing is requested.
24
+ - Typed wildcard ranges reject invalid candidate versions.
25
+ - APK comparison and validation now follow apk-tools tokens instead of Gentoo version rules.
26
+ - The minimum supported Ruby version is now 3.3.
27
+
28
+ ### Fixed
29
+
30
+ - Unioning an unbounded interval with a bounded interval now remains unbounded.
31
+ - Range unions retain exclusions when the other operand does not contain the excluded version.
32
+ - VERS serialization no longer adds an empty constraint to unbounded ranges with exclusions.
33
+ - SemVer comparison handles surrounding whitespace consistently with validation.
34
+ - npm partial and prerelease ranges retain their membership after VERS serialization.
35
+ - Cargo and PyPI intersections follow their scheme-specific build and local version equality rules.
36
+
10
37
  ## [1.1.0] - 2026-02-24
11
38
 
12
39
  ### Added
@@ -126,7 +153,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
126
153
  - **No runtime dependencies** - pure Ruby implementation
127
154
  - **Minitest** for testing (development dependency only)
128
155
 
129
- [Unreleased]: https://github.com/andrew/vers/compare/v1.1.0...HEAD
156
+ [Unreleased]: https://github.com/andrew/vers/compare/v2.0.0...HEAD
157
+ [2.0.0]: https://github.com/andrew/vers/compare/v1.3.1...v2.0.0
130
158
  [1.1.0]: https://github.com/andrew/vers/compare/v1.0.3...v1.1.0
131
159
  [1.0.3]: https://github.com/andrew/vers/compare/v1.0.2...v1.0.3
132
160
  [1.0.2]: https://github.com/andrew/vers/compare/v1.0.1...v1.0.2
data/CONTRIBUTING.md CHANGED
@@ -41,7 +41,7 @@ Enhancement suggestions are welcome! Please:
41
41
 
42
42
  ### Prerequisites
43
43
 
44
- - Ruby 3.2 or higher
44
+ - Ruby 3.3 or higher
45
45
  - Bundler gem
46
46
 
47
47
  ### Setup
@@ -191,4 +191,4 @@ Provide clear, actionable error messages:
191
191
  - Suggest corrections when possible
192
192
  - Use specific exception types
193
193
 
194
- Thank you for contributing to make Vers better for everyone!
194
+ Thank you for contributing to make Vers better for everyone!
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # Vers - Version Range Parser for Ruby
2
2
 
3
- A Ruby library for parsing, comparing and sorting versions according to the [VERS specification](https://github.com/package-url/purl-spec/blob/main/VERSION-RANGE-SPEC.rst).
3
+ A Ruby library for parsing VERS ranges and applying package-manager version rules. It supports canonical VERS strings, native range syntax, scheme-aware comparison, validation, normalization, and release classification.
4
4
 
5
- This gem provides tools for working with version ranges across different package managers, using a mathematical interval model internally and supporting the vers specification from the Package URL (PURL) project.
6
-
7
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](https://www.ruby-lang.org/)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-red.svg)](https://www.ruby-lang.org/)
8
6
  [![Gem Version](https://badge.fury.io/rb/vers.svg)](https://rubygems.org/gems/vers)
9
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
8
 
@@ -12,14 +10,11 @@ This gem provides tools for working with version ranges across different package
12
10
 
13
11
  ## Features
14
12
 
15
- - **Universal version range parsing** with support for 6 package ecosystems (npm, gem, pypi, maven, debian, rpm)
16
- - **Mathematical interval model** for precise set operations (union, intersection, complement)
17
- - **VERS specification compliance** with full support for the Package URL version range specification
18
- - **Native syntax support** - parse native package manager syntax (^1.2.3, ~>1.0, >=1.0,<2.0, [1.0,2.0))
19
- - **Bidirectional conversion** between native syntax and universal vers URI format
20
- - **Semantic versioning features** - version increment, constraint checking, prerelease handling
21
- - **Comprehensive error handling** with detailed parsing exceptions
22
- - **100% test coverage** with 113 tests and 366 assertions
13
+ - Parse and serialize canonical VERS strings.
14
+ - Parse native ranges for npm, Cargo, RubyGems, PyPI, Composer, Pub, Maven, NuGet, Hex, Go, Debian, RPM, Conan, OpenSSL, and nginx.
15
+ - Compare, validate, normalize, clean, and classify versions with package-manager rules.
16
+ - Combine ranges with union, intersection, complement, and exclusions.
17
+ - Check containment with the range's version scheme.
23
18
 
24
19
  ## Installation
25
20
 
@@ -56,29 +51,37 @@ npm_range = Vers.parse_native("^1.2.3", "npm")
56
51
  gem_range = Vers.parse_native("~> 1.0", "gem")
57
52
 
58
53
  # Check version containment
59
- Vers.satisfies?("1.5.0", ">=1.0.0,<2.0.0") # => true
54
+ Vers.satisfies?("1.5.0", ">=1.0.0,<2.0.0", "pypi") # => true
60
55
 
61
56
  # Compare versions
62
57
  Vers.compare("1.2.3", "1.2.4") # => -1
63
58
 
59
+ # Use Bazel's module version rules
60
+ Vers.compare_with_scheme("0.7.1", "0.7.1.bcr.1", "bazel") # => -1
61
+ Vers.valid?("35.1", "bazel") # => true
62
+ Vers.normalize("v01.02", "composer") # => "1.2.0"
63
+ Vers.clean(" v1.2 ", "npm") # => "1.2.0"
64
+ Vers.stable?("0.7.1.bcr.1", "bazel") # => true
65
+ Vers.prerelease?("36.0-rc2", "bazel") # => true
66
+
64
67
  # Version operations
65
68
  version = Vers::Version.new("1.2.3")
66
69
  version.increment_major # => #<Vers::Version "2.0.0">
67
70
  version.satisfies?("~> 1.2") # => true
68
71
  ```
69
72
 
70
- ## Supported Package Managers
73
+ ## Version Schemes
74
+
75
+ Scheme-aware version operations support these groups:
76
+
77
+ - SemVer family: SemVer, npm, Cargo, Go modules, Hex, and nginx.
78
+ - Language registries: RubyGems, PyPI, Composer, Pub, Maven, and NuGet.
79
+ - Distribution packages: Debian, RPM, APK, Gentoo, and ALPM.
80
+ - Other formats: Bazel, Conan, OpenSSL, integer-dot, RFC 3339 datetime, and lexicographic versions.
71
81
 
72
- - **npm** (Node.js): Caret ranges (^1.2.3), tilde ranges (~1.2.3), hyphen ranges (1.2.3 - 2.3.4), OR logic (||), wildcards (1.x, *)
73
- - **RubyGems** (Ruby): Pessimistic operator (~> 1.2), standard operators (>=, <=, etc.), comma-separated constraints
74
- - **PyPI** (Python): Comma-separated constraints (>=1.0,<2.0), exclusions (!=1.5.0), compatible release (~=1.4.2)
75
- - **Maven** (Java): Bracket notation ([1.0,2.0], (1.0,2.0)), union ranges, open ranges
76
- - **NuGet** (.NET): Bracket notation ([1.0,2.0], (1.0,2.0)), mixed brackets, open ranges
77
- - **Packagist** (PHP Composer): Caret ranges (^1.2.3), tilde ranges (~1.2), stability flags (@dev, @alpha)
78
- - **Debian** (apt): Standard comparison operators (>=1.0.0, <<2.0.0)
79
- - **RPM** (yum/dnf): Standard comparison operators (>=1.0.0, <=2.0.0)
82
+ The aliases `rubygems`, `debian`, `golang`, `elixir`, and `alpine` map to their canonical schemes. Unknown schemes keep the generic version behavior. Bazel is an implementation-defined scheme in this library and remains separate from the VERS specification types.
80
83
 
81
- Many other package managers are also supported using standard comparison operators (>=, <=, <, >, =, !=), including Cargo (Rust), Go modules, and more.
84
+ Native range parsing includes npm and Cargo caret, tilde, wildcard, hyphen, AND, and OR forms; Composer stability and branch forms; Pub caret ranges; RubyGems pessimistic ranges; PyPI specifiers; Maven and NuGet bracket ranges; Conan compatible ranges; OpenSSL exact-version lists; and nginx plus ranges. Other schemes accept standard VERS comparison operators.
82
85
 
83
86
  ## Mathematical Model
84
87
 
@@ -134,7 +137,7 @@ vers_string = Vers.to_vers_string(npm_range, "npm")
134
137
  puts vers_string # => "vers:npm/>=1.2.3|<2.0.0"
135
138
 
136
139
  # Parse vers URI and use in your application
137
- range = Vers.parse("vers:gem/~>1.0")
140
+ range = Vers.parse_native("~>1.0", "gem")
138
141
  puts range.contains?("1.5.0") # => true
139
142
  ```
140
143
 
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vers
4
+ module BazelVersion
5
+ VERSION_REGEX = /\A(?<release>[a-zA-Z0-9.]+)(?:-(?<prerelease>[a-zA-Z0-9.-]+))?(?:\+[a-zA-Z0-9.-]+)?\z/
6
+ MAX_NUMERIC_IDENTIFIER = (2**64) - 1
7
+
8
+ Identifier = Struct.new(:numeric, :number, :text, keyword_init: true)
9
+ ParsedVersion = Struct.new(:release, :prerelease, :empty, keyword_init: true)
10
+
11
+ module_function
12
+
13
+ def compare(a, b)
14
+ return 0 if a == b
15
+
16
+ version_a = parse(a)
17
+ version_b = parse(b)
18
+
19
+ return 1 if version_a.empty && !version_b.empty
20
+ return -1 if !version_a.empty && version_b.empty
21
+
22
+ release_comparison = compare_identifiers(version_a.release, version_b.release)
23
+ return release_comparison unless release_comparison == 0
24
+
25
+ return 1 if version_a.prerelease.empty? && !version_b.prerelease.empty?
26
+ return -1 if !version_a.prerelease.empty? && version_b.prerelease.empty?
27
+
28
+ compare_identifiers(version_a.prerelease, version_b.prerelease)
29
+ end
30
+
31
+ def valid?(version)
32
+ return false if version == ""
33
+
34
+ parse(version)
35
+ true
36
+ rescue ArgumentError
37
+ false
38
+ end
39
+
40
+ def stable?(version)
41
+ parse(version).prerelease.empty?
42
+ rescue ArgumentError
43
+ false
44
+ end
45
+
46
+ def prerelease?(version)
47
+ !parse(version).prerelease.empty?
48
+ rescue ArgumentError
49
+ false
50
+ end
51
+
52
+ def normalize(version)
53
+ raise ArgumentError, "Invalid Bazel version: #{version.inspect}" unless valid?(version)
54
+
55
+ version.split("+", 2).first
56
+ end
57
+
58
+ def parse(version)
59
+ raise ArgumentError, "Invalid Bazel version: #{version.inspect}" unless version.is_a?(String)
60
+
61
+ if version.empty?
62
+ return ParsedVersion.new(release: [], prerelease: [], empty: true)
63
+ end
64
+
65
+ match = VERSION_REGEX.match(version)
66
+ raise ArgumentError, "Invalid Bazel version: #{version.inspect}" unless match
67
+
68
+ ParsedVersion.new(
69
+ release: parse_identifiers(match[:release], version),
70
+ prerelease: match[:prerelease] ? parse_identifiers(match[:prerelease], version) : [],
71
+ empty: false
72
+ )
73
+ end
74
+
75
+ def parse_identifiers(value, version)
76
+ value.split(".", -1).map do |identifier|
77
+ raise ArgumentError, "Invalid Bazel version: #{version}" if identifier.empty?
78
+
79
+ if identifier.match?(/\A[0-9]+\z/)
80
+ number = identifier.to_i
81
+ if number > MAX_NUMERIC_IDENTIFIER
82
+ raise ArgumentError, "Numeric Bazel version identifier is too large: #{identifier}"
83
+ end
84
+
85
+ Identifier.new(numeric: true, number: number, text: identifier)
86
+ else
87
+ Identifier.new(numeric: false, number: 0, text: identifier)
88
+ end
89
+ end
90
+ end
91
+
92
+ def compare_identifiers(identifiers_a, identifiers_b)
93
+ [identifiers_a.length, identifiers_b.length].min.times do |index|
94
+ comparison = compare_identifier(identifiers_a[index], identifiers_b[index])
95
+ return comparison unless comparison == 0
96
+ end
97
+
98
+ identifiers_a.length <=> identifiers_b.length
99
+ end
100
+
101
+ def compare_identifier(identifier_a, identifier_b)
102
+ if identifier_a.numeric != identifier_b.numeric
103
+ return identifier_a.numeric ? -1 : 1
104
+ end
105
+
106
+ if identifier_a.numeric
107
+ number_comparison = identifier_a.number <=> identifier_b.number
108
+ return number_comparison unless number_comparison == 0
109
+ end
110
+
111
+ identifier_a.text <=> identifier_b.text
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version_comparison"
4
+
5
+ module Vers
6
+ module ComposerVersion
7
+ extend self
8
+
9
+ PATTERN = /\Av?([0-9]+(?:\.[0-9]+){0,3})(?:[-._]?([a-z]+)(?:[.-]?([0-9]+(?:[.-][0-9]+)*))?)?(?:\+\S+)?\z/i
10
+ NUMERIC_BRANCH_PATTERN = /\Av?\d+(?:\.\d+)*\.x-dev\z/i
11
+ Parsed = Data.define(:core, :stability, :number)
12
+
13
+ STABILITIES = {
14
+ "dev" => 0,
15
+ "a" => 1,
16
+ "alpha" => 1,
17
+ "b" => 2,
18
+ "beta" => 2,
19
+ "rc" => 3,
20
+ "stable" => 4,
21
+ "p" => 5,
22
+ "pl" => 5,
23
+ "patch" => 5
24
+ }.freeze
25
+
26
+ def compare(left, right)
27
+ parsed_left = parse(left)
28
+ parsed_right = parse(right)
29
+ left_branch = branch?(left)
30
+ right_branch = branch?(right)
31
+
32
+ return -1 if left_branch && !right_branch
33
+ return 1 if !left_branch && right_branch
34
+ return left.downcase <=> right.downcase unless parsed_left && parsed_right
35
+
36
+ parsed_left.core.zip(parsed_right.core).each do |left_part, right_part|
37
+ comparison = VersionComparison.compare_numbers(left_part, right_part)
38
+ return comparison unless comparison.zero?
39
+ end
40
+
41
+ comparison = parsed_left.stability <=> parsed_right.stability
42
+ return comparison unless comparison.zero?
43
+
44
+ compare_numbers(parsed_left.number, parsed_right.number)
45
+ end
46
+
47
+ def parse(value)
48
+ match = PATTERN.match(value.to_s.strip)
49
+ return nil unless match
50
+
51
+ core = match[1].split(".")
52
+ core << "0" while core.length < 4
53
+ qualifier = match[2]&.downcase
54
+ stability = qualifier ? STABILITIES[qualifier] : 4
55
+ return nil unless stability
56
+
57
+ Parsed.new(core.freeze, stability, match[3].to_s)
58
+ end
59
+
60
+ def branch?(value)
61
+ string = value.to_s.strip
62
+ (string.downcase.start_with?("dev-") && string.length > 4) || string.match?(NUMERIC_BRANCH_PATTERN)
63
+ end
64
+
65
+ def valid?(value)
66
+ string = value.to_s.strip
67
+ !string.empty? && !string.match?(/[[:space:]]/) && (!parse(string).nil? || branch?(string))
68
+ end
69
+
70
+ def normalize(value, implicit_stability: nil)
71
+ string = value.to_s.strip
72
+ return string if branch?(string)
73
+
74
+ match = PATTERN.match(string.sub(/\Av(?=\d)/i, ""))
75
+ raise ArgumentError, "Invalid Composer version: #{value}" unless match
76
+
77
+ core = match[1].split(".").map { |part| VersionComparison.normalize_number(part) }
78
+ core << "0" while core.length < 3
79
+ qualifier = match[2]&.downcase || implicit_stability
80
+ qualifier = {"a" => "alpha", "b" => "beta", "rc" => "RC", "p" => "patch", "pl" => "patch"}.fetch(qualifier, qualifier)
81
+ return core.join(".") if qualifier.nil? || qualifier == "stable"
82
+
83
+ "#{core.join(".")}-#{qualifier}#{match[3]}"
84
+ end
85
+
86
+ def release_parts(value)
87
+ match = PATTERN.match(value.to_s.strip.sub(/\Av(?=\d)/i, ""))
88
+ return nil unless match
89
+
90
+ match[1].split(".")
91
+ end
92
+
93
+ def explicit_stability?(value)
94
+ match = PATTERN.match(value.to_s.strip.sub(/\Av(?=\d)/i, ""))
95
+ match && !match[2].nil?
96
+ end
97
+
98
+ def prerelease?(value)
99
+ return true if branch?(value)
100
+
101
+ parsed = parse(value)
102
+ parsed && parsed.stability < STABILITIES.fetch("stable")
103
+ end
104
+
105
+ def compare_numbers(left, right)
106
+ left_parts = left.split(/[.-]/)
107
+ right_parts = right.split(/[.-]/)
108
+
109
+ [left_parts.length, right_parts.length].max.times do |index|
110
+ comparison = VersionComparison.compare_numbers(left_parts[index], right_parts[index])
111
+ return comparison unless comparison.zero?
112
+ end
113
+
114
+ 0
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version_comparison"
4
+
5
+ module Vers
6
+ module ConanVersion
7
+ extend self
8
+
9
+ def compare(left, right)
10
+ left_main, left_pre, left_build = split(left)
11
+ right_main, right_pre, right_build = split(right)
12
+
13
+ comparison = compare_main(left_main, right_main)
14
+ return comparison unless comparison.zero?
15
+
16
+ comparison = compare_optional(left_pre, right_pre, prerelease: true)
17
+ return comparison unless comparison.zero?
18
+
19
+ compare_optional(left_build, right_build, prerelease: false)
20
+ end
21
+
22
+ def split(value)
23
+ main = value.to_s
24
+ build = nil
25
+ prerelease = nil
26
+
27
+ separator = main.rindex("+")
28
+ if separator
29
+ build = main[(separator + 1)..]
30
+ main = main[0...separator]
31
+ end
32
+
33
+ separator = main.rindex("-")
34
+ if separator
35
+ prerelease = main[(separator + 1)..]
36
+ main = main[0...separator]
37
+ end
38
+
39
+ [main, prerelease, build]
40
+ end
41
+
42
+ def valid?(value)
43
+ string = value.to_s.strip
44
+ !string.empty? && !string.match?(/[[:space:]]/)
45
+ end
46
+
47
+ def normalize(value)
48
+ value.to_s.strip
49
+ end
50
+
51
+ def prerelease?(value)
52
+ !split(value.to_s.strip)[1].nil?
53
+ end
54
+
55
+ def compare_main(left, right)
56
+ left_parts = significant_parts(left)
57
+ right_parts = significant_parts(right)
58
+
59
+ left_parts.zip(right_parts).each do |left_part, right_part|
60
+ break unless left_part && right_part
61
+
62
+ comparison = if VersionComparison.numeric?(left_part) && VersionComparison.numeric?(right_part)
63
+ VersionComparison.compare_numbers(left_part, right_part)
64
+ else
65
+ left_part <=> right_part
66
+ end
67
+ return comparison unless comparison.zero?
68
+ end
69
+
70
+ left_parts.length <=> right_parts.length
71
+ end
72
+
73
+ def significant_parts(value)
74
+ parts = value.split(".", -1)
75
+ parts.pop while parts.last && VersionComparison.numeric?(parts.last) && VersionComparison.compare_numbers(parts.last, "0").zero?
76
+ parts
77
+ end
78
+
79
+ def compare_optional(left, right, prerelease:)
80
+ return 0 if left.nil? && right.nil?
81
+ return prerelease ? 1 : -1 if left.nil?
82
+ return prerelease ? -1 : 1 if right.nil?
83
+
84
+ compare(left, right)
85
+ end
86
+ end
87
+ end
@@ -131,8 +131,12 @@ module Vers
131
131
  # @param version_string [String] The version to check
132
132
  # @return [Boolean] true if the version satisfies the constraint
133
133
  #
134
- def satisfies?(version_string)
135
- comparison = Version.compare(version_string, version)
134
+ def satisfies?(version_string, scheme: nil)
135
+ comparison = if scheme
136
+ Version.compare_with_scheme(version_string, version, scheme)
137
+ else
138
+ Version.compare(version_string, version)
139
+ end
136
140
 
137
141
  case operator
138
142
  when "="
@@ -167,4 +171,4 @@ module Vers
167
171
  [operator, version].hash
168
172
  end
169
173
  end
170
- end
174
+ end