Class: Vers::Version

Inherits:
Object
  • Object
show all
Defined in:
lib/vers/version.rb

Overview

Handles version comparison and normalization across different package ecosystems.

This class provides version comparison functionality that can handle different versioning schemes used by various package managers (npm, gem, pypi, etc.).

Examples

Vers::Version.compare("1.2.3", "1.2.4")     # => -1
Vers::Version.compare("2.0.0", "1.9.9")     # => 1
Vers::Version.compare("1.0.0", "1.0.0")     # => 0

Constant Summary collapse

SEMANTIC_VERSION_REGEX =

Regex for parsing semantic version components including build metadata

/\A(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([^+]+))?(?:\+(.+))?\z/
MAX_LENGTH =

Maximum accepted length for a version string. Real-world version strings rarely exceed 100 characters; 256 leaves headroom for unusual prerelease tags while bounding regex/split work and cache key size.

256
@@version_cache =

Cache for parsed versions to avoid repeated parsing

{}
@@cache_size_limit =
2000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(version_string) ⇒ Version

Creates a new Version object

Parameters:

  • version_string (String)

    The version string to parse

Raises:

  • (ArgumentError)

    if the version string exceeds MAX_LENGTH



38
39
40
41
42
43
44
# File 'lib/vers/version.rb', line 38

def initialize(version_string)
  @original = version_string.to_s
  if @original.length > MAX_LENGTH
    raise ArgumentError, "Version string too long (#{@original.length} > #{MAX_LENGTH})"
  end
  parse_version
end

Instance Attribute Details

#buildObject (readonly)

Returns the value of attribute build.



30
31
32
# File 'lib/vers/version.rb', line 30

def build
  @build
end

#majorObject (readonly)

Returns the value of attribute major.



30
31
32
# File 'lib/vers/version.rb', line 30

def major
  @major
end

#minorObject (readonly)

Returns the value of attribute minor.



30
31
32
# File 'lib/vers/version.rb', line 30

def minor
  @minor
end

#patchObject (readonly)

Returns the value of attribute patch.



30
31
32
# File 'lib/vers/version.rb', line 30

def patch
  @patch
end

#prereleaseObject (readonly)

Returns the value of attribute prerelease.



30
31
32
# File 'lib/vers/version.rb', line 30

def prerelease
  @prerelease
end

Class Method Details

.cached_new(version_string) ⇒ Version

Creates a new Version object with caching

Parameters:

  • version_string (String)

    The version string to parse

Returns:

  • (Version)

    Cached or new Version object



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/vers/version.rb', line 52

def self.cached_new(version_string)
  # Skip caching for oversized keys to bound cache memory by entry
  # count, not by attacker-controlled key length.
  return new(version_string) if version_string.to_s.length > MAX_LENGTH

  if @@version_cache.size >= @@cache_size_limit
    # Keep the most recent half instead of clearing everything
    keys = @@version_cache.keys
    keys.first(keys.size / 2).each { |k| @@version_cache.delete(k) }
  end

  @@version_cache[version_string] ||= new(version_string)
end

.clean(version_string) ⇒ Object



124
125
126
127
# File 'lib/vers/version.rb', line 124

def self.clean(version_string)
  return nil unless valid?(version_string)
  version_string.to_s.sub(/\Av/, '')
end

.compare(a, b) ⇒ Integer

Compares two version strings

Parameters:

  • a (String)

    First version string

  • b (String)

    Second version string

Returns:

  • (Integer)

    -1 if a < b, 0 if a == b, 1 if a > b



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/vers/version.rb', line 73

def self.compare(a, b)
  return 0 if a == b
  return -1 if a.nil?
  return 1 if b.nil?

  # Use cached versions for better performance
  version_a = cached_new(a)
  version_b = cached_new(b)

  version_a <=> version_b
end

.compare_with_scheme(a, b, scheme) ⇒ Integer

Compares two version strings using scheme-specific rules

Parameters:

  • a (String)

    First version string

  • b (String)

    Second version string

  • scheme (String, nil)

    Package manager scheme (maven, nuget, or nil for generic)

Returns:

  • (Integer)

    -1 if a < b, 0 if a == b, 1 if a > b



93
94
95
96
97
98
99
100
101
102
# File 'lib/vers/version.rb', line 93

def self.compare_with_scheme(a, b, scheme)
  case scheme
  when "maven"
    MavenVersion.compare(a, b)
  when "nuget"
    NuGetVersion.compare(a, b)
  else
    compare(a, b)
  end
end

.normalize(version_string) ⇒ String

Normalizes a version string to a consistent format

Parameters:

  • version_string (String)

    The version string to normalize

Returns:

  • (String)

    The normalized version string



110
111
112
# File 'lib/vers/version.rb', line 110

def self.normalize(version_string)
  cached_new(version_string).to_s
end

.valid?(version_string) ⇒ Boolean

Checks if a version string is valid

Parameters:

  • version_string (String)

    The version string to validate

Returns:

  • (Boolean)

    true if the version is valid



120
121
122
# File 'lib/vers/version.rb', line 120

def self.valid?(version_string)
  version_string.to_s.match?(/\Av?\d+\.\d+\.\d+/)
end

Instance Method Details

#<(other) ⇒ Object



175
176
177
# File 'lib/vers/version.rb', line 175

def <(other)
  (self <=> other) < 0
end

#<=(other) ⇒ Object



179
180
181
# File 'lib/vers/version.rb', line 179

def <=(other)
  (self <=> other) <= 0
end

#<=>(other) ⇒ Integer

Version comparison operator

Parameters:

  • other (Version)

    The other version to compare to

Returns:

  • (Integer)

    -1, 0, or 1



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/vers/version.rb', line 135

def <=>(other)
  return 0 if @original == other.to_s

  # Compare major.minor.patch numerically
  major_cmp = (major || 0) <=> (other.major || 0)
  return major_cmp unless major_cmp == 0

  minor_cmp = (minor || 0) <=> (other.minor || 0)
  return minor_cmp unless minor_cmp == 0

  patch_cmp = (patch || 0) <=> (other.patch || 0)
  return patch_cmp unless patch_cmp == 0

  # Handle prerelease comparison
  return 1 if prerelease.nil? && !other.prerelease.nil?
  return -1 if !prerelease.nil? && other.prerelease.nil?
  return 0 if prerelease.nil? && other.prerelease.nil?

  compare_prerelease(prerelease, other.prerelease)
end

#==(other) ⇒ Object



171
172
173
# File 'lib/vers/version.rb', line 171

def ==(other)
  other.is_a?(Version) && (self <=> other) == 0
end

#>(other) ⇒ Object



183
184
185
# File 'lib/vers/version.rb', line 183

def >(other)
  (self <=> other) > 0
end

#>=(other) ⇒ Object



187
188
189
# File 'lib/vers/version.rb', line 187

def >=(other)
  (self <=> other) >= 0
end

#baseVersion

Creates a new Version with the same major.minor but patch set to 0

Returns:

  • (Version)

    A new Version object with patch reset to 0



328
329
330
# File 'lib/vers/version.rb', line 328

def base
  self.class.new("#{major}.#{minor || 0}.0")
end

#hashObject



191
192
193
# File 'lib/vers/version.rb', line 191

def hash
  [@original].hash
end

#increment(component) ⇒ Version

Increments the specified component of the version

Examples

version = Vers::Version.new("1.2.3")
version.increment(:major)  # => #<Vers::Version "2.0.0">
version.increment(:minor)  # => #<Vers::Version "1.3.0">
version.increment(:patch)  # => #<Vers::Version "1.2.4">

Parameters:

  • component (Symbol)

    The component to increment (:major, :minor, :patch)

Returns:

  • (Version)

    A new Version object with the incremented component



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/vers/version.rb', line 208

def increment(component)
  case component
  when :major
    self.class.new("#{major + 1}.0.0")
  when :minor
    self.class.new("#{major}.#{(minor || 0) + 1}.0")
  when :patch
    self.class.new("#{major}.#{minor || 0}.#{(patch || 0) + 1}")
  else
    raise ArgumentError, "Invalid component: #{component}. Must be :major, :minor, or :patch"
  end
end

#increment_majorVersion

Increments the major version component

Returns:

  • (Version)

    A new Version object with incremented major version



226
227
228
# File 'lib/vers/version.rb', line 226

def increment_major
  increment(:major)
end

#increment_minorVersion

Increments the minor version component

Returns:

  • (Version)

    A new Version object with incremented minor version



235
236
237
# File 'lib/vers/version.rb', line 235

def increment_minor
  increment(:minor)
end

#increment_patchVersion

Increments the patch version component

Returns:

  • (Version)

    A new Version object with incremented patch version



244
245
246
# File 'lib/vers/version.rb', line 244

def increment_patch
  increment(:patch)
end

#prerelease?Boolean

Checks if this is a prerelease version

Returns:

  • (Boolean)

    true if this is a prerelease version



304
305
306
# File 'lib/vers/version.rb', line 304

def prerelease?
  !prerelease.nil?
end

#satisfies?(constraint) ⇒ Boolean

Checks if this version satisfies a constraint using pessimistic operator logic

Examples

version = Vers::Version.new("1.2.5")
version.satisfies?("~> 1.2")    # => true (>= 1.2.0, < 1.3.0)
version.satisfies?("~> 1.2.3")  # => true (>= 1.2.3, < 1.3.0)
version.satisfies?("~> 1.3")    # => false

Parameters:

  • constraint (String)

    The constraint string (e.g., "~> 1.2")

Returns:

  • (Boolean)

    true if this version satisfies the constraint



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/vers/version.rb', line 261

def satisfies?(constraint)
  if constraint.start_with?("~>")
    # Pessimistic constraint
    base_version = constraint.sub(/^~>\s*/, "").strip
    base = self.class.new(base_version)
    
    # Must be >= base version
    return false if self < base
    
    # Must be < next significant version
    if base.patch && base.patch > 0
      # ~> 1.2.3 means >= 1.2.3, < 1.3.0
      upper_bound = self.class.new("#{base.major}.#{(base.minor || 0) + 1}.0")
    elsif base.minor
      # ~> 1.2 means >= 1.2.0, < 1.3.0  
      upper_bound = self.class.new("#{base.major}.#{(base.minor || 0) + 1}.0")
    else
      # ~> 1 means >= 1.0.0, < 2.0.0
      upper_bound = self.class.new("#{base.major + 1}.0.0")
    end
    
    self < upper_bound
  else
    # For other constraints, delegate to constraint parsing
    # This would require the Constraint class, so for now return true
    true
  end
end

#stable?Boolean

Checks if this is a stable release (no prerelease components)

Returns:

  • (Boolean)

    true if this is a stable release



295
296
297
# File 'lib/vers/version.rb', line 295

def stable?
  prerelease.nil?
end

#to_hHash

Gets the semantic version components as a hash

Returns:

  • (Hash)

    Hash with :major, :minor, :patch, :prerelease, :build keys



313
314
315
316
317
318
319
320
321
# File 'lib/vers/version.rb', line 313

def to_h
  {
    major: major,
    minor: minor,
    patch: patch,
    prerelease: prerelease,
    build: build
  }
end

#to_sString

String representation of the version

Returns:

  • (String)

    The normalized version string



161
162
163
164
165
166
167
168
169
# File 'lib/vers/version.rb', line 161

def to_s
  @to_s ||= begin
    version = "#{major || 0}"
    version += ".#{minor || 0}"
    version += ".#{patch || 0}"
    version += "-#{prerelease}" if prerelease
    version.freeze
  end
end