lib/dry/logic/predicates.rb



# frozen_string_literal: true

require "dry/core/constants"

require "bigdecimal"
require "bigdecimal/util"
require "date"
require "uri"

module Dry
  module Logic
    module Predicates
      include ::Dry::Core::Constants

      # rubocop:disable Metrics/ModuleLength
      module Methods
        def self.uuid_format(version)
          ::Regexp.new(<<~FORMAT.chomp, ::Regexp::IGNORECASE)
            \\A[0-9A-F]{8}-[0-9A-F]{4}-#{version}[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\\z
          FORMAT
        end

        UUIDv1 = uuid_format(1)

        UUIDv2 = uuid_format(2)

        UUIDv3 = uuid_format(3)

        UUIDv4 = uuid_format(4)

        UUIDv5 = uuid_format(5)

        UUIDv6 = uuid_format(6)

        UUIDv7 = uuid_format(7)

        UUIDv8 = uuid_format(8)

        def [](name)
          method(name)
        end

        def type?(type, input) = input.is_a?(type)

        def nil?(input) = input.nil?
        alias_method :none?, :nil?

        def key?(name, input) = input.key?(name)

        def attr?(name, input) = input.respond_to?(name)

        def empty?(input)
          case input
          when ::String, ::Array, ::Hash then input.empty?
          when nil then true
          else
            false
          end
        end

        def filled?(input) = !empty?(input)

        def bool?(input) = input.equal?(true) || input.equal?(false)

        def date?(input) = input.is_a?(::Date)

        def date_time?(input) = input.is_a?(::DateTime)

        def time?(input) = input.is_a?(::Time)

        def number?(input)
          true if Float(input)
        rescue ::ArgumentError, ::TypeError
          false
        end

        def int?(input) = input.is_a?(::Integer)

        def float?(input) = input.is_a?(::Float)

        def decimal?(input) = input.is_a?(::BigDecimal)

        def str?(input) = input.is_a?(::String)

        def hash?(input) = input.is_a?(::Hash)

        def array?(input) = input.is_a?(::Array)

        def odd?(input) = input.odd?

        def even?(input) = input.even?

        def lt?(num, input) = input < num

        def gt?(num, input) = input > num

        def lteq?(num, input) = !gt?(num, input)

        def gteq?(num, input) = !lt?(num, input)

        def size?(size, input)
          case size
          when ::Integer then size.equal?(input.size)
          when ::Range, ::Array then size.include?(input.size)
          else
            raise ::ArgumentError, "+#{size}+ is not supported type for size? predicate."
          end
        end

        def min_size?(num, input) = input.size >= num

        def max_size?(num, input) = input.size <= num

        def bytesize?(size, input)
          case size
          when ::Integer then size.equal?(input.bytesize)
          when ::Range, ::Array then size.include?(input.bytesize)
          else
            raise ::ArgumentError, "+#{size}+ is not supported type for bytesize? predicate."
          end
        end

        def min_bytesize?(num, input) = input.bytesize >= num

        def max_bytesize?(num, input) = input.bytesize <= num

        def inclusion?(list, input)
          deprecated(:inclusion?, :included_in?)
          included_in?(list, input)
        end

        def exclusion?(list, input)
          deprecated(:exclusion?, :excluded_from?)
          excluded_from?(list, input)
        end

        def included_in?(list, input) = list.include?(input)

        def excluded_from?(list, input) = !list.include?(input)

        def includes?(value, input)
          if input.respond_to?(:include?)
            input.include?(value)
          else
            false
          end
        rescue ::TypeError
          false
        end

        def excludes?(value, input) = !includes?(value, input)

        # This overrides Object#eql? so we need to make it compatible
        def eql?(left, right = Undefined)
          return super(left) if right.equal?(Undefined)

          left.eql?(right)
        end

        def is?(left, right) = left.equal?(right)

        def not_eql?(left, right) = !left.eql?(right)

        def true?(value) = value.equal?(true)

        def false?(value) = value.equal?(false)

        def format?(regex, input) = !input.nil? && regex.match?(input)

        def case?(pattern, input) = pattern === input # rubocop:disable Style/CaseEquality

        def uuid_v1?(input) = format?(UUIDv1, input)

        def uuid_v2?(input) = format?(UUIDv2, input)

        def uuid_v3?(input) = format?(UUIDv3, input)

        def uuid_v4?(input) = format?(UUIDv4, input)

        def uuid_v5?(input) = format?(UUIDv5, input)

        def uuid_v6?(input) = format?(UUIDv6, input)

        def uuid_v7?(input) = format?(UUIDv7, input)

        def uuid_v8?(input) = format?(UUIDv8, input)

        if defined?(::URI::RFC2396_PARSER)
          def uri?(schemes, input)
            uri_format = ::URI::RFC2396_PARSER.make_regexp(schemes)
            format?(uri_format, input)
          end
        else
          def uri?(schemes, input)
            uri_format = ::URI::DEFAULT_PARSER.make_regexp(schemes)
            format?(uri_format, input)
          end
        end

        def uri_rfc3986?(input) = format?(::URI::RFC3986_Parser::RFC3986_URI, input)

        # This overrides Object#respond_to? so we need to make it compatible
        def respond_to?(method, input = Undefined)
          return super if input.equal?(Undefined)

          input.respond_to?(method)
        end

        def predicate(name, &)
          define_singleton_method(name, &)
        end

        def deprecated(name, in_favor_of)
          Core::Deprecations.warn(
            "#{name} predicate is deprecated and will " \
            "be removed in the next major version\n" \
            "Please use #{in_favor_of} predicate instead",
            tag: "dry-logic",
            uplevel: 3
          )
        end
      end

      extend Methods

      def self.included(other)
        super
        other.extend(Methods)
      end
    end
    # rubocop:enable Metrics/ModuleLength
  end
end