lib/dry/schema/types_merger.rb



# frozen_string_literal: true

module Dry
  module Schema
    # Combines multiple logical operations into a single type, taking into
    # account the type of logical operation (or, and, implication) and the
    # underlying types (schemas, nominals, etc.)
    #
    # @api private
    class TypesMerger
      attr_reader :type_registry

      # @api private
      class ValueMerger
        attr_reader :types_merger
        attr_reader :op_class
        attr_reader :old
        attr_reader :new

        # @api private
        def initialize(types_merger, op_class, old, new)
          @types_merger = types_merger
          @op_class = op_class
          @old = old
          @new = new
        end

        # @api private
        def call
          if op_class <= Dry::Logic::Operations::Or
            merge_or
          elsif op_class <= Dry::Logic::Operations::And
            merge_and
          elsif op_class <= Dry::Logic::Operations::Implication
            merge_implication
          else
            raise ArgumentError, <<~MESSAGE
              Can't merge operations, op_class=#{op_class}
            MESSAGE
          end
        end

        private

        # @api private
        def merge_or
          old | new
        end

        # @api private
        def merge_ordered
          return old if old == new

          unwrapped_old, old_rule = unwrap_type(old)
          unwrapped_new, new_rule = unwrap_type(new)

          type = merge_unwrapped_types(unwrapped_old, unwrapped_new)

          rule = [old_rule, new_rule].compact.reduce { op_class.new(_1, _2) }

          type = Dry::Types::Constrained.new(type, rule: rule) if rule

          type
        end

        alias_method :merge_and, :merge_ordered
        alias_method :merge_implication, :merge_ordered

        # @api private
        def merge_unwrapped_types(unwrapped_old, unwrapped_new)
          case [unwrapped_old, unwrapped_new]
          in Dry::Types::Schema, Dry::Types::Schema
            merge_schemas(unwrapped_old, unwrapped_new)
          in [Dry::Types::AnyClass, _] | [Dry::Types::Hash, Dry::Types::Schema]
            unwrapped_new
          in [Dry::Types::Schema, Dry::Types::Hash] | [_, Dry::Types::AnyClass]
            unwrapped_old
          else
            merge_equivalent_types(unwrapped_old, unwrapped_new)
          end
        end

        # @api private
        def merge_schemas(unwrapped_old, unwrapped_new)
          types_merger.type_registry["hash"].schema(
            types_merger.call(
              op_class,
              unwrapped_old.name_key_map,
              unwrapped_new.name_key_map
            )
          )
        end

        # @api private
        def merge_equivalent_types(unwrapped_old, unwrapped_new)
          if unwrapped_old.primitive <= unwrapped_new.primitive
            unwrapped_new
          elsif unwrapped_new.primitive <= unwrapped_old.primitive
            unwrapped_old
          else
            raise ArgumentError, <<~MESSAGE
              Can't merge types, unwrapped_old=#{unwrapped_old.inspect}, unwrapped_new=#{unwrapped_new.inspect}
            MESSAGE
          end
        end

        # @api private
        def unwrap_type(type)
          rules = []

          loop do
            rules << type.rule if type.respond_to?(:rule)

            if type.optional?
              type = type.left.primitive?(nil) ? type.right : type.left
            elsif type.is_a?(Dry::Types::Decorator)
              type = type.type
            else
              break
            end
          end

          [type, rules.reduce(:&)]
        end
      end

      def initialize(type_registry = TypeRegistry.new)
        @type_registry = type_registry
      end

      # @api private
      def call(op_class, lhs, rhs)
        lhs.merge(rhs) do |_k, old, new|
          ValueMerger.new(self, op_class, old, new).call
        end
      end
    end
  end
end