module Solve
class Constraint
class << self
# Coerce the object into a constraint.
#
# @param [Constraint, String]
#
# @return [Constraint]
def coerce(object)
if object.nil?
Semverse::DEFAULT_CONSTRAINT
else
object.is_a?(self) ? object : new(object)
end
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)
if constraint =~ /^[0-9]/
operator = "="
version = constraint
else
_, operator, version = REGEXP.match(constraint).to_a
end
if operator.nil?
raise Errors::InvalidConstraintFormat.new(constraint)
end
split_version = case version.to_s
when /^(\d+)\.(\d+)\.(\d+)(-([0-9a-z\-\.]+))?(\+([0-9a-z\-\.]+))?$/i
[ $1.to_i, $2.to_i, $3.to_i, $5, $7 ]
when /^(\d+)\.(\d+)\.(\d+)?$/
[ $1.to_i, $2.to_i, $3.to_i, nil, nil ]
when /^(\d+)\.(\d+)?$/
[ $1.to_i, $2.to_i, nil, nil, nil ]
when /^(\d+)$/
[ $1.to_i, nil, nil, nil, nil ]
else
raise Errors::InvalidConstraintFormat.new(constraint)
end
[ operator, split_version ].flatten
end
# @param [Semverse::Constraint] constraint
# @param [Semverse::Version] target_version
#
# @return [Boolean]
def compare_equal(constraint, target_version)
target_version == constraint.version
end
# @param [Semverse::Constraint] constraint
# @param [Semverse::Version] target_version
#
# @return [Boolean]
def compare_gt(constraint, target_version)
target_version > constraint.version
end
# @param [Semverse::Constraint] constraint
# @param [Semverse::Version] target_version
#
# @return [Boolean]
def compare_lt(constraint, target_version)
target_version < constraint.version
end
# @param [Semverse::Constraint] constraint
# @param [Semverse::Version] target_version
#
# @return [Boolean]
def compare_gte(constraint, target_version)
target_version >= constraint.version
end
# @param [Semverse::Constraint] constraint
# @param [Semverse::Version] target_version
#
# @return [Boolean]
def compare_lte(constraint, target_version)
target_version <= constraint.version
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?
Semverse::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
Semverse::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
Semverse::Version.new([min.major, min.minor, min.patch, identifiers.fill(replace, -1).join(".")])
else
Semverse::Version.new([min.major, min.minor + 1, 0, 0])
end
min <= target_version && target_version < max
end
end
OPERATOR_TYPES = {
"~>" => :approx,
"~" => :approx,
">=" => :greater_than_equal,
"<=" => :less_than_equal,
"=" => :equal,
">" => :greater_than,
"<" => :less_than,
}.freeze
COMPARE_FUNS = {
approx: method(:compare_approx),
greater_than_equal: method(:compare_gte),
greater_than: method(:compare_gt),
less_than_equal: method(:compare_lte),
less_than: method(:compare_lt),
equal: method(:compare_equal),
}.freeze
REGEXP = /^(#{OPERATOR_TYPES.keys.join('|')})\s?(.+)$/.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 = nil)
constraint = constraint.to_s
if constraint.nil? || constraint.empty?
constraint = ">= 0.0.0"
end
@operator, @major, @minor, @patch, @pre_release, @build = self.class.split(constraint)
unless operator_type == :approx
@minor ||= 0
@patch ||= 0
end
@version = Semverse::Version.new([
major,
minor,
patch,
pre_release,
build,
])
end
# @return [Symbol]
def operator_type
unless ( type = OPERATOR_TYPES.fetch(operator) )
raise "unknown operator type: #{operator}"
end
type
end
# Returns true or false if the given version would be satisfied by
# the version constraint.
#
# @param [Semverse::Version, #to_s] target
#
# @return [Boolean]
def satisfies?(target)
target = Semverse::Version.coerce(target)
return false if !(version == 0) && greedy_match?(target)
compare(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) &&
operator == other.operator &&
version == other.version
end
alias_method :eql?, :==
def inspect
"#<#{self.class} #{self}>"
end
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)
operator_type !~ /less/ && target_version.pre_release? && !version.pre_release?
end
# @param [Semverse::Version] target
#
# @return [Boolean]
def compare(target)
COMPARE_FUNS.fetch(operator_type).call(self, target)
end
end
end