lib/dry/schema/key.rb



# frozen_string_literal: true

module Dry
  module Schema
    # Key objects used by key maps
    #
    # @api public
    class Key
      extend Dry::Core::Cache

      DEFAULT_COERCER = :itself.to_proc.freeze

      include Dry.Equalizer(:name, :coercer)

      # @return [Symbol] The key identifier
      attr_reader :id

      # @return [Symbol, String, Object] The actual key name expected in an input hash
      attr_reader :name

      # @return [Proc, #call] A key name coercer function
      attr_reader :coercer

      # @api private
      def self.[](name, **opts)
        new(name, **opts)
      end

      # @api private
      def self.new(*args, **kwargs)
        fetch_or_store(args, kwargs) { super }
      end

      # @api private
      def initialize(id, name: id, coercer: DEFAULT_COERCER)
        @id = id
        @name = name
        @coercer = coercer
      end

      # @api private
      def read(source)
        return unless source.is_a?(::Hash)

        if source.key?(name)
          yield(source[name])
        elsif source.key?(coerced_name)
          yield(source[coerced_name])
        end
      end

      # @api private
      def write(source, target)
        read(source) { |value| target[coerced_name] = value }
      end

      # @api private
      def coercible(&coercer)
        new(coercer: coercer)
      end

      # @api private
      def stringified
        new(name: name.to_s)
      end

      # @api private
      def to_dot_notation
        [name.to_s]
      end

      # @api private
      def new(**new_opts)
        self.class.new(id, name: name, coercer: coercer, **new_opts)
      end

      # @api private
      def dump
        name
      end

      private

      # @api private
      def coerced_name
        @__coerced_name__ ||= coercer[name]
      end
    end

    # A specialized key type which handles nested hashes
    #
    # @api private
    class Key::Hash < Key
      include Dry.Equalizer(:name, :members, :coercer)

      # @api private
      attr_reader :members

      # @api private
      def initialize(id, members:, **opts)
        super(id, **opts)
        @members = members
      end

      # @api private
      def read(source)
        super if source.is_a?(::Hash)
      end

      def write(source, target)
        read(source) { |value|
          target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
        }
      end

      # @api private
      def coercible(&coercer)
        new(coercer: coercer, members: members.coercible(&coercer))
      end

      # @api private
      def stringified
        new(name: name.to_s, members: members.stringified)
      end

      # @api private
      def to_dot_notation
        [name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
      end

      # @api private
      def dump
        {name => members.map(&:dump)}
      end
    end

    # A specialized key type which handles nested arrays
    #
    # @api private
    class Key::Array < Key
      include Dry.Equalizer(:name, :member, :coercer)

      attr_reader :member

      # @api private
      def initialize(id, member:, **opts)
        super(id, **opts)
        @member = member
      end

      # @api private
      def write(source, target)
        read(source) { |value|
          target[coerced_name] = value.is_a?(::Array) ? value.map { |el| member.write(el) } : value
        }
      end

      # @api private
      def coercible(&coercer)
        new(coercer: coercer, member: member.coercible(&coercer))
      end

      # @api private
      def stringified
        new(name: name.to_s, member: member.stringified)
      end

      # @api private
      def to_dot_notation
        [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
      end

      # @api private
      def dump
        [name, member.dump]
      end
    end
  end
end