module Semverse
class Constraint
class << self
# Coerce the object into a constraint.
#
# @param [Constraint, String]
#
# @return [Constraint]
def coerce(object)
if object.nil?
DEFAULT_CONSTRAINT
else
object.is_a?(self) ? object : new(object)
end
end
# Returns all of the versions which satisfy all of the given constraints
#
# @param [Array<Semverse::Constraint>, Array<String>] constraints
# @param [Array<Semverse::Version>, Array<String>] versions
#
# @return [Array<Semverse::Version>]
def satisfy_all(constraints, versions)
constraints = Array(constraints).collect do |con|
con.is_a?(Constraint) ? con : Constraint.new(con)
end.uniq
versions = Array(versions).collect do |ver|
ver.is_a?(Version) ? ver : Version.new(ver)
end.uniq
versions.select do |ver|
constraints.all? { |constraint| constraint.satisfies?(ver) }
end
end
# Return the best version from the given list of versions for the given list of constraints
#
# @param [Array<Semverse::Constraint>, Array<String>] constraints
# @param [Array<Semverse::Version>, Array<String>] versions
#
# @raise [NoSolutionError] if version matches the given constraints
#
# @return [Semverse::Version]
def satisfy_best(constraints, versions)
solution = satisfy_all(constraints, versions)
if solution.empty?
raise NoSolutionError
end
solution.sort.last
end
# Split a constraint string into an Array of two elements. The first
# element being the operator and second being the version string.
#
# If the given string does not contain a constraint operator then (=)
# will be used.
#
# If the given string does not contain a valid version string then
# nil will be returned.
#
# @param [#to_s] constraint
#
# @example splitting a string with a constraint operator and valid version string
# Constraint.split(">= 1.0.0") => [ ">=", "1.0.0" ]
#
# @example splitting a string without a constraint operator
# Constraint.split("0.0.0") => [ "=", "1.0.0" ]
#
# @example splitting a string without a valid version string
# Constraint.split("hello") => nil
#
# @return [Array, nil]
def split(constraint)
data = REGEX.match(constraint)
if data.nil?
raise InvalidConstraintFormat.new(constraint)
else
[
data[:operator] || DEFAULT_OPERATOR,
data[:major].to_i,
data[:minor] && data[:minor].to_i,
data[:patch] && data[:patch].to_i,
data[:pre_release],
data[:build],
]
end
end
# @param [Semverse::Constraint] constraint
# @param [Semverse::Version] target_version
#
# @return [Boolean]
def compare_approx(constraint, target_version)
min = constraint.version
max = if constraint.patch.nil?
Version.new([min.major + 1, 0, 0, 0])
elsif constraint.build
identifiers = constraint.version.identifiers(:build)
replace = identifiers.last.to_i.to_s == identifiers.last.to_s ? "-" : nil
Version.new([min.major, min.minor, min.patch, min.pre_release, identifiers.fill(replace, -1).join('.')])
elsif constraint.pre_release
identifiers = constraint.version.identifiers(:pre_release)
replace = identifiers.last.to_i.to_s == identifiers.last.to_s ? "-" : nil
Version.new([min.major, min.minor, min.patch, identifiers.fill(replace, -1).join('.')])
else
Version.new([min.major, min.minor + 1, 0, 0])
end
min <= target_version && target_version < max
end
end
# The default constraint string.
#
# @return [String]
DEFAULT_OPERATOR = '='.freeze
# The complete list of possible operators, paired with a proc to be used for
# evaluation.
#
# @example
# OPERATORS['='].call(constraint, version)
#
# @return [Hash<String, Proc>]
OPERATORS = { #:nodoc:
'=' => ->(c, v) { v == c.version },
'!=' => ->(c, v) { v != c.version },
'>' => ->(c, v) { v > c.version },
'<' => ->(c, v) { v < c.version },
'>=' => ->(c, v) { v >= c.version },
'<=' => ->(c, v) { v <= c.version },
'~' => method(:compare_approx),
'~>' => method(:compare_approx),
}.freeze
# This is a magical regular expression that matches the Semantic versioning
# specification found at http://semver.org. In addition to supporting all
# the possible versions, it also provides a named +match_data+ which makes
# it really delightful to work with.
#
# @return [Regexp]
REGEX = /\A
((?<operator>(#{OPERATORS.keys.join('|')}))[[:space:]]*)?
(?<major>\d+)
(\.(?<minor>\d+))?
(\.(?<patch>\d+))?
(\-(?<pre_release>[0-9A-Za-z\-\.]+))?
(\+(?<build>[0-9A-Za-z\-\.]+))?
\z/x.freeze
attr_reader :operator
attr_reader :major
attr_reader :minor
attr_reader :patch
attr_reader :pre_release
attr_reader :build
# Return the Semverse::Version representation of the major, minor, and patch
# attributes of this instance
#
# @return [Semverse::Version]
attr_reader :version
# @param [#to_s] constraint
def initialize(constraint = '>= 0.0.0')
@operator, @major, @minor, @patch, @pre_release, @build = self.class.split(constraint)
unless ['~>', '~'].include?(@operator)
@minor ||= 0
@patch ||= 0
end
@version = Version.new([
self.major,
self.minor,
self.patch,
self.pre_release,
self.build,
])
end
# Returns true or false if the given version would be satisfied by
# the version constraint.
#
# @param [Version, #to_s] target
#
# @return [Boolean]
def satisfies?(target)
target = Version.coerce(target)
if !version.zero? && greedy_match?(target)
return false
end
OPERATORS[operator].call(self, target)
end
# dep-selector uses include? to determine if a version matches the
# constriant.
alias_method :include?, :satisfies?
# @param [Object] other
#
# @return [Boolean]
def ==(other)
other.is_a?(self.class) &&
self.operator == other.operator &&
self.version == other.version
end
alias_method :eql?, :==
# The detailed string representation of this constraint.
#
# @return [String]
def inspect
"#<#{self.class.to_s} #{to_s}>"
end
# The string representation of this constraint.
#
# @return [String]
def to_s
out = "#{operator} #{major}"
out << ".#{minor}" if minor
out << ".#{patch}" if patch
out << "-#{pre_release}" if pre_release
out << "+#{build}" if build
out
end
private
# Returns true if the given version is a pre-release and if the constraint
# does not include a pre-release and if the operator isn't < or <=.
# This avoids greedy matches, e.g. 2.0.0.alpha won't satisfy >= 1.0.0.
#
# @param [Semverse::Version] target_version
#
def greedy_match?(target_version)
!['<', '<='].include?(self.operator) &&
target_version.pre_release? &&
!version.pre_release?
end
end
end