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



51
52
53
54
55
56
57
# File 'lib/vers/version.rb', line 51

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.



43
44
45
# File 'lib/vers/version.rb', line 43

def build
  @build
end

#majorObject (readonly)

Returns the value of attribute major.



43
44
45
# File 'lib/vers/version.rb', line 43

def major
  @major
end

#minorObject (readonly)

Returns the value of attribute minor.



43
44
45
# File 'lib/vers/version.rb', line 43

def minor
  @minor
end

#patchObject (readonly)

Returns the value of attribute patch.



43
44
45
# File 'lib/vers/version.rb', line 43

def patch
  @patch
end

#prereleaseObject (readonly)

Returns the value of attribute prerelease.



43
44
45
# File 'lib/vers/version.rb', line 43

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



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/vers/version.rb', line 65

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, scheme = nil) ⇒ Object



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

def self.clean(version_string, scheme = nil)
  return nil unless valid?(version_string, scheme)
  return normalize(version_string, scheme) if Scheme.handler(scheme)

  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



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/vers/version.rb', line 86

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_for_range(a, b, scheme) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/vers/version.rb', line 115

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

  case Scheme.canonical(scheme)
  when "cargo"
    SemverVersion.compare(a, b)
  when "pypi"
    PyPIVersion.compare_public(a, b)
  else
    compare_with_scheme(a, b, scheme)
  end
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 (bazel, maven, nuget, or nil for generic)

Returns:

  • (Integer)

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



106
107
108
109
110
111
112
113
# File 'lib/vers/version.rb', line 106

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

  handler = Scheme.handler(scheme)
  handler ? handler.compare(a, b) : compare(a, b)
end

.normalize(version_string, scheme = nil) ⇒ String

Normalizes a version string to a consistent format

Parameters:

  • version_string (String)

    The version string to normalize

Returns:

  • (String)

    The normalized version string

Raises:

  • (ArgumentError)


136
137
138
139
140
141
142
143
# File 'lib/vers/version.rb', line 136

def self.normalize(version_string, scheme = nil)
  handler = Scheme.handler(scheme)
  return cached_new(version_string).to_s unless handler

  raise ArgumentError, "Invalid #{Scheme.canonical(scheme)} version: #{version_string}" unless handler.valid?(version_string)

  handler.respond_to?(:normalize) ? handler.normalize(version_string) : version_string.to_s.strip
end

.prerelease?(version_string, scheme = nil) ⇒ Boolean

Returns:

  • (Boolean)


168
169
170
171
172
173
174
175
# File 'lib/vers/version.rb', line 168

def self.prerelease?(version_string, scheme = nil)
  handler = Scheme.handler(scheme)
  return handler.valid?(version_string) && handler.prerelease?(version_string) if handler

  cached_new(version_string).prerelease?
rescue ArgumentError
  false
end

.stable?(version_string, scheme = nil) ⇒ Boolean

Returns:

  • (Boolean)


159
160
161
162
163
164
165
166
# File 'lib/vers/version.rb', line 159

def self.stable?(version_string, scheme = nil)
  handler = Scheme.handler(scheme)
  return handler.valid?(version_string) && !handler.prerelease?(version_string) if handler

  cached_new(version_string).stable?
rescue ArgumentError
  false
end

.valid?(version_string, scheme = nil) ⇒ Boolean

Checks if a version string is valid

Parameters:

  • version_string (String)

    The version string to validate

  • scheme (String, nil) (defaults to: nil)

    Package manager scheme or nil for generic validation

Returns:

  • (Boolean)

    true if the version is valid



152
153
154
155
156
157
# File 'lib/vers/version.rb', line 152

def self.valid?(version_string, scheme = nil)
  handler = Scheme.handler(scheme)
  return handler.valid?(version_string) if handler

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

Instance Method Details

#<(other) ⇒ Object



230
231
232
# File 'lib/vers/version.rb', line 230

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

#<=(other) ⇒ Object



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

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



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/vers/version.rb', line 190

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



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

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

#>(other) ⇒ Object



238
239
240
# File 'lib/vers/version.rb', line 238

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

#>=(other) ⇒ Object



242
243
244
# File 'lib/vers/version.rb', line 242

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



383
384
385
# File 'lib/vers/version.rb', line 383

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

#hashObject



246
247
248
# File 'lib/vers/version.rb', line 246

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



263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/vers/version.rb', line 263

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



281
282
283
# File 'lib/vers/version.rb', line 281

def increment_major
  increment(:major)
end

#increment_minorVersion

Increments the minor version component

Returns:

  • (Version)

    A new Version object with incremented minor version



290
291
292
# File 'lib/vers/version.rb', line 290

def increment_minor
  increment(:minor)
end

#increment_patchVersion

Increments the patch version component

Returns:

  • (Version)

    A new Version object with incremented patch version



299
300
301
# File 'lib/vers/version.rb', line 299

def increment_patch
  increment(:patch)
end

#prerelease?Boolean

Checks if this is a prerelease version

Returns:

  • (Boolean)

    true if this is a prerelease version



359
360
361
# File 'lib/vers/version.rb', line 359

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



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/vers/version.rb', line 316

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



350
351
352
# File 'lib/vers/version.rb', line 350

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



368
369
370
371
372
373
374
375
376
# File 'lib/vers/version.rb', line 368

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



216
217
218
219
220
221
222
223
224
# File 'lib/vers/version.rb', line 216

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