class SemanticPuppet::VersionRange

A Semantic Version Range.

@see github.com/npm/node-semver for full specification @api public

Constants

ALL_RANGE
EMPTY_RANGE

A range that matches no versions

HYPHEN
HYPHEN_EXPR
LOGICAL_OR
LOWER_X
NR
PART
PARTIAL
PARTIAL_EXPR
PARTS
QUALIFIER
QUALIFIER_NC
RANGE_SPLIT
SIMPLE

The ~> isn't in the spec but allowed

SIMPLE_EXPR
SIMPLE_WITH_EXTRA_WS
SIMPLE_WITH_EXTRA_WS_EXPR
STAR
UPPER_X
XR
XR_NC

Attributes

ranges[R]

Provides read access to the ranges. For internal use only @api private

Public Class Methods

new(ranges, string, exclude_end = false) click to toggle source

Creates a new version range @overload initialize(from, to, exclude_end = false)

Creates a new instance using ruby `Range` semantics
@param begin [String,Version] the version denoting the start of the range (always inclusive)
@param end [String,Version] the version denoting the end of the range
@param exclude_end [Boolean] `true` if the `end` version should be excluded from the range

@overload initialize(ranges, string)

Creates a new instance based on parsed content. For internal use only
@param ranges [Array<AbstractRange>] the ranges to include in this range
@param string [String] the original string representation that was parsed to produce the ranges

@api private

# File lib/semantic_puppet/version_range.rb, line 224
def initialize(ranges, string, exclude_end = false)
  unless ranges.is_a?(Array)
    lb = GtEqRange.new(ranges)
    if exclude_end
      ub = LtRange.new(string)
      string = ">=#{string} <#{ranges}"
    else
      ub = LtEqRange.new(string)
      string = "#{string} - #{ranges}"
    end
    ranges = [MinMaxRange.create(lb, ub)]
  end
  ranges.compact!

  merge_happened = true
  while ranges.size > 1 && merge_happened
    # merge ranges if possible
    merge_happened = false
    result = []
    until ranges.empty?
      unmerged = []
      x = ranges.pop
      result << ranges.reduce(x) do |memo, y|
        merged = memo.merge(y)
        if merged.nil?
          unmerged << y
        else
          merge_happened = true
          memo = merged
        end
        memo
      end
      ranges = unmerged
    end
    ranges = result.reverse!
  end

  ranges = [LtRange::MATCH_NOTHING] if ranges.empty?
  @ranges = ranges
  @string = string.nil? ? ranges.join(' || ') : string
end
parse(range_string) click to toggle source

Parses a version range string into a comparable {VersionRange} instance.

Currently parsed version range string may take any of the following: forms:

  • Regular Semantic Version strings

    • ex. `“1.0.0”`, `“1.2.3-pre”`

  • Partial Semantic Version strings

    • ex. `“1.0.x”`, `“1”`, `“2.X”`, `“3.*”`,

  • Inequalities

    • ex. `“> 1.0.0”`, `“<3.2.0”`, `“>=4.0.0”`

  • Approximate Caret Versions

    • ex. `“^1”`, `“^3.2”`, `“^4.1.0”`

  • Approximate Tilde Versions

    • ex. `“~1.0.0”`, `“~ 3.2.0”`, `“~4.0.0”`

  • Inclusive Ranges

    • ex. `“1.0.0 - 1.3.9”`

  • Range Intersections

    • ex. `“>1.0.0 <=2.3.0”`

  • Combined ranges

    • ex, `“>=1.0.0 <2.3.0 || >=2.5.0 <3.0.0”`

@param range_string [String] the version range string to parse @return [VersionRange] a new {VersionRange} instance @api public

# File lib/semantic_puppet/version_range.rb, line 64
def self.parse(range_string)
  # Remove extra whitespace after operators. Such whitespace should not cause a split
  range_set = range_string.gsub(/([><=~^])(?:\s+|\s*v)/, '\1')
  ranges = range_set.split(LOGICAL_OR)
  return ALL_RANGE if ranges.empty?

  new(ranges.map do |range|
    if range =~ HYPHEN_EXPR
      MinMaxRange.create(GtEqRange.new(parse_version($1)), LtEqRange.new(parse_version($2)))
    else
      # Split on whitespace
      simples = range.split(RANGE_SPLIT).map do |simple|
        match_data = SIMPLE_EXPR.match(simple)
        raise ArgumentError, "Unparsable version range: \"#{range_string}\"" unless match_data
        operand = match_data[2]

        # Case based on operator
        case match_data[1]
        when '~', '~>', '~='
          parse_tilde(operand)
        when '^'
          parse_caret(operand)
        when '>'
          parse_gt_version(operand)
        when '>='
          GtEqRange.new(parse_version(operand))
        when '<'
          LtRange.new(parse_version(operand))
        when '<='
          parse_lteq_version(operand)
        when '='
          parse_xrange(operand)
        else
          parse_xrange(operand)
        end
      end
      simples.size == 1 ? simples[0] : MinMaxRange.create(*simples)
    end
  end.uniq, range_string).freeze
end

Private Class Methods

allow_minor_updates(major, match_data) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 147
def self.allow_minor_updates(major, match_data)
  return AllRange.SINGLETON unless major
  minor = digit(match_data[2])
  if minor
    patch = digit(match_data[3])
    if patch.nil?
      MinMaxRange.new(GtEqRange.new(Version.new(major, minor, 0)), LtRange.new(Version.new(major + 1, 0, 0)))
    else
      if match_data[4].nil?
        MinMaxRange.new(GtEqRange.new(Version.new(major, minor, patch)), LtRange.new(Version.new(major + 1, 0, 0)))
      else
        MinMaxRange.new(
          GtEqRange.new(
            Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5]))),
          LtRange.new(Version.new(major + 1, 0, 0)))
      end
    end
  else
    MinMaxRange.new(GtEqRange.new(Version.new(major, 0, 0)), LtRange.new(Version.new(major + 1, 0, 0)))
  end
end
allow_patch_updates(major, match_data, tilde_or_caret = true) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 131
def self.allow_patch_updates(major, match_data, tilde_or_caret = true)
  return AllRange::SINGLETON unless major

  minor = digit(match_data[2])
  return MinMaxRange.new(GtEqRange.new(Version.new(major, 0, 0)), LtRange.new(Version.new(major + 1, 0, 0))) unless minor

  patch = digit(match_data[3])
  return MinMaxRange.new(GtEqRange.new(Version.new(major, minor, 0)), LtRange.new(Version.new(major, minor + 1, 0))) unless patch

  version = Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5]))
  return EqRange.new(version) unless tilde_or_caret

  MinMaxRange.new(GtEqRange.new(version), LtRange.new(Version.new(major, minor + 1, 0)))
end
digit(str) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 170
def self.digit(str)
  (str.nil? || UPPER_X == str || LOWER_X == str || STAR == str) ? nil : str.to_i
end
parse_caret(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 112
def self.parse_caret(expr)
  match_data = parse_partial(expr)
  major = digit(match_data[1])
  major == 0 ? allow_patch_updates(major, match_data) : allow_minor_updates(major, match_data)
end
parse_gt_version(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 184
def self.parse_gt_version(expr)
  match_data = parse_partial(expr)
  major = digit(match_data[1])
  return LtRange::MATCH_NOTHING unless major
  minor = digit(match_data[2])
  return GtEqRange.new(Version.new(major + 1, 0, 0)) unless minor
  patch = digit(match_data[3])
  return GtEqRange.new(Version.new(major, minor + 1, 0)) unless patch
  return GtRange.new(Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5])))
end
parse_lteq_version(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 196
def self.parse_lteq_version(expr)
  match_data = parse_partial(expr)
  major = digit(match_data[1])
  return AllRange.SINGLETON unless major
  minor = digit(match_data[2])
  return LtRange.new(Version.new(major + 1, 0, 0)) unless minor
  patch = digit(match_data[3])
  return LtRange.new(Version.new(major, minor + 1, 0)) unless patch
  return LtEqRange.new(Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5])))
end
parse_partial(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 105
def self.parse_partial(expr)
  match_data = PARTIAL_EXPR.match(expr)
  raise ArgumentError, "Unparsable version range: \"#{expr}\"" unless match_data
  match_data
end
parse_tilde(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 119
def self.parse_tilde(expr)
  match_data = parse_partial(expr)
  allow_patch_updates(digit(match_data[1]), match_data)
end
parse_version(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 175
def self.parse_version(expr)
  match_data = parse_partial(expr)
  major = digit(match_data[1]) || 0
  minor = digit(match_data[2]) || 0
  patch = digit(match_data[3]) || 0
  Version.new(major, minor, patch, Version.parse_prerelease(match_data[4]), Version.parse_build(match_data[5]))
end
parse_xrange(expr) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 125
def self.parse_xrange(expr)
  match_data = parse_partial(expr)
  allow_patch_updates(digit(match_data[1]), match_data, false)
end

Public Instance Methods

&(other)
Alias for: intersection
==(range)
Alias for: eql?
===(version)
Alias for: include?
begin() click to toggle source

Returns the version that denotes the beginning of this range.

Since this really is an OR between disparate ranges, it may have multiple beginnings. This method returns `nil` if that is the case.

@return [Version] the beginning of the range, or `nil` if there are multiple beginnings @api public

# File lib/semantic_puppet/version_range.rb, line 282
def begin
  @ranges.size == 1 ? @ranges[0].begin : nil
end
cover?(version)
Alias for: include?
end() click to toggle source

Returns the version that denotes the end of this range.

Since this really is an OR between disparate ranges, it may have multiple ends. This method returns `nil` if that is the case.

@return [Version] the end of the range, or `nil` if there are multiple ends @api public

# File lib/semantic_puppet/version_range.rb, line 293
def end
  @ranges.size == 1 ? @ranges[0].end : nil
end
eql?(range) click to toggle source
# File lib/semantic_puppet/version_range.rb, line 266
def eql?(range)
  range.is_a?(VersionRange) && @ranges.eql?(range.ranges)
end
Also aliased as: ==
exclude_begin?() click to toggle source

Returns `true` if the beginning is excluded from the range.

Since this really is an OR between disparate ranges, it may have multiple beginnings. This method returns `nil` if that is the case.

@return [Boolean] `true` if the beginning is excluded from the range, `false` if included, or `nil` if there are multiple beginnings @api public

# File lib/semantic_puppet/version_range.rb, line 304
def exclude_begin?
  @ranges.size == 1 ? @ranges[0].exclude_begin? : nil
end
exclude_end?() click to toggle source

Returns `true` if the end is excluded from the range.

Since this really is an OR between disparate ranges, it may have multiple ends. This method returns `nil` if that is the case.

@return [Boolean] `true` if the end is excluded from the range, `false` if not, or `nil` if there are multiple ends @api public

# File lib/semantic_puppet/version_range.rb, line 315
def exclude_end?
  @ranges.size == 1 ? @ranges[0].exclude_end? : nil
end
hash() click to toggle source
# File lib/semantic_puppet/version_range.rb, line 271
def hash
  @ranges.hash
end
include?(version) click to toggle source

@return [Boolean] `true` if the given version is included in the range @api public

# File lib/semantic_puppet/version_range.rb, line 321
def include?(version)
  @ranges.any? { |range| range.include?(version) && (version.stable? || range.test_prerelease?(version)) }
end
Also aliased as: member?, cover?, ===
inspect() click to toggle source

Returns a canonical string representation of this range, assembled from the internal matchers.

@return [String] a range expression representing this VersionRange @api public

# File lib/semantic_puppet/version_range.rb, line 357
def inspect
  @ranges.join(' || ')
end
intersection(other) click to toggle source

Computes the intersection of a pair of ranges. If the ranges have no useful intersection, an empty range is returned.

@param other [VersionRange] the range to intersect with @return [VersionRange] the common subset @api public

# File lib/semantic_puppet/version_range.rb, line 334
def intersection(other)
  raise ArgumentError, "value must be a #{self.class.name}" unless other.is_a?(VersionRange)
  result = @ranges.map { |range| other.ranges.map { |o_range| range.intersection(o_range) } }.flatten
  result.compact!
  result.uniq!
  result.empty? ? EMPTY_RANGE : VersionRange.new(result, nil)
end
Also aliased as: &
member?(version)
Alias for: include?
to_s() click to toggle source

Returns a string representation of this range. This will be the string that was used when the range was parsed.

@return [String] a range expression representing this VersionRange @api public

# File lib/semantic_puppet/version_range.rb, line 348
def to_s
  @string
end