lib/solve/constraint.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Solve
  # @author Jamie Winsor <jamie@vialstudios.com>
  # @author Thibaud Guillaume-Gentil <thibaud@thibaud.me>
  class Constraint
    class << self
      # 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] string
      #
      # @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(string)
        if string =~ /^[0-9]/
          op = "="
          ver = string
        else
          _, op, ver = REGEXP.match(string).to_a
        end

        return nil unless op || ver

        [ op, ver ]
      end

      # @param [Solve::Constraint] constraint
      # @param [Solve::Version] target_version
      #
      # @return [Boolean]
      def compare_equal(constraint, target_version)
        target_version == constraint.version
      end

      # @param [Solve::Constraint] constraint
      # @param [Solve::Version] target_version
      #
      # @return [Boolean]
      def compare_gt(constraint, target_version)
        target_version > constraint.version
      end

      # @param [Solve::Constraint] constraint
      # @param [Solve::Version] target_version
      #
      # @return [Boolean]
      def compare_lt(constraint, target_version)
        target_version < constraint.version
      end

      # @param [Solve::Constraint] constraint
      # @param [Solve::Version] target_version
      #
      # @return [Boolean]
      def compare_gte(constraint, target_version)
        target_version >= constraint.version
      end

      # @param [Solve::Constraint] constraint
      # @param [Solve::Version] target_version
      #
      # @return [Boolean]
      def compare_lte(constraint, target_version)
        target_version <= constraint.version
      end

      # @param [Solve::Constraint] constraint
      # @param [Solve::Version] target_version
      #
      # @return [Boolean]
      def compare_aprox(constraint, target_version)
        min = constraint.version
        if constraint.patch == nil
          max = 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
          max = 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
          max = Version.new([min.major, min.minor, min.patch, identifiers.fill(replace, -1).join('.')])
        else
          max = Version.new([min.major, min.minor + 1, 0, 0])
        end
        min <= target_version && target_version < max
      end
    end

    OPERATORS = {
      "~>" => method(:compare_aprox),
      ">=" => method(:compare_gte),
      "<=" => method(:compare_lte),
      "=" => method(:compare_equal),
      "~" => method(:compare_aprox),
      ">" => method(:compare_gt),
      "<" => method(:compare_lt)
    }.freeze

    REGEXP = /^(#{OPERATORS.keys.join('|')})\s?(.+)$/

    attr_reader :operator
    attr_reader :major
    attr_reader :minor
    attr_reader :patch
    attr_reader :pre_release
    attr_reader :build

    # @param [#to_s] constraint (">= 0.0.0")
    def initialize(constraint = nil)
      if constraint.nil? || constraint.empty?
        constraint = ">= 0.0.0"
      end

      @operator, ver_str = self.class.split(constraint)
      if @operator.nil? || ver_str.nil?
        raise Errors::InvalidConstraintFormat.new(constraint)
      end

      @major, @minor, @patch, @pre_release, @build = Version.split(ver_str)
      @compare_fun = OPERATORS.fetch(self.operator)
    end

    # Return the Solve::Version representation of the major, minor, and patch
    # attributes of this instance
    #
    # @return [Solve::Version]
    def version
      @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 [#to_s] target_version
    #
    # @return [Boolean]
    def satisfies?(target_version)
      target_version = Version.new(target_version.to_s)

      @compare_fun.call(self, target_version)
    end

    # @param [Object] other
    #
    # @return [Boolean]
    def ==(other)
      other.is_a?(self.class) &&
        self.operator == other.operator &&
        self.version == other.version
    end
    alias_method :eql?, :==

    def to_s
      str = operator
      str += " #{major}.#{minor}.#{patch}"
      str += "-#{pre_release}" if pre_release
      str += "+#{build}" if build
      str
    end
  end
end