# frozen_string_literal: true

require 'rom/initializer'
require 'rom/support/memoizable'
require 'rom/types'

module ROM
  # Schema attributes provide meta information about types and an API
  # for additional operations. This class can be extended by adapters to provide
  # database-specific features. In example rom-sql provides SQL::Attribute
  # with more features like creating SQL expressions for queries.
  #
  # Schema attributes are accessible through canonical relation schemas and
  # instance-level schemas.
  #
  # @api public
  class Attribute
    include Dry::Equalizer(:type, :options)
    include Memoizable

    extend Initializer

    META_OPTIONS = %i[primary_key foreign_key source target relation].freeze

    # @!attribute [r] type
    #   @return [Dry::Types::Nominal, Dry::Types::Sum, Dry::Types::Constrained] The attribute's
    #                                                                           type object
    param :type

    # @!attribute [r] name
    #
    # Return the canonical name of this attribute name
    #
    # This *always* returns the name that is used in the datastore, even when
    # an attribute is aliased
    #
    # @example
    #   class Users < ROM::Relation[:memory]
    #     schema do
    #       attribute :user_id, Types::Integer, alias: :id
    #       attribute :email, Types::String
    #     end
    #   end
    #
    #   users[:user_id].name
    #   # => :user_id
    #
    #   users[:email].name
    #   # => :email
    #
    # @return [Symbol]
    #
    # @api public
    option :name, optional: true, type: Types::Strict::Symbol

    # @!attribute [r] type
    #   @return [Symbol, nil] Alias to use instead of attribute name
    option :alias, optional: true, type: Types::Strict::Symbol.optional

    # @api private
    def [](value = Undefined)
      type[value]
    end

    # Return true if this attribute type is a primary key
    #
    # @example
    #   class Users < ROM::Relation[:memory]
    #     schema do
    #       attribute :id, Types::Integer
    #       attribute :name, Types::String
    #
    #       primary_key :id
    #     end
    #   end
    #
    #   Users.schema[:id].primary_key?
    #   # => true
    #
    #   Users.schema[:name].primary_key?
    #   # => false
    #
    # @return [TrueClass,FalseClass]
    #
    # @api public
    def primary_key? = meta[:primary_key].equal?(true)

    # Return true if this attribute type is a foreign key
    #
    # @example
    #   class Tasks < ROM::Relation[:memory]
    #     schema do
    #       attribute :id, Types::Integer
    #       attribute :user_id, Types.ForeignKey(:users)
    #     end
    #   end
    #
    #   Users.schema[:user_id].foreign_key?
    #   # => true
    #
    #   Users.schema[:id].foreign_key?
    #   # => false
    #
    # @return [TrueClass,FalseClass]
    #
    # @api public
    def foreign_key? = meta[:foreign_key].equal?(true)

    # Return true if this attribute has a configured alias
    #
    # @example
    #   class Tasks < ROM::Relation[:memory]
    #     schema do
    #       attribute :user_id, Types::Integer, alias: :id
    #       attribute :name, Types::String
    #     end
    #   end
    #
    #   Users.schema[:user_id].aliased?
    #   # => true
    #
    #   Users.schema[:name].aliased?
    #   # => false
    #
    # @return [TrueClass,FalseClass]
    #
    # @api public
    def aliased? = !self.alias.nil?

    # Return source relation of this attribute type
    #
    # @example
    #   class Tasks < ROM::Relation[:memory]
    #     schema do
    #       attribute :id, Types::Integer
    #       attribute :user_id, Types.ForeignKey(:users)
    #     end
    #   end
    #
    #   Users.schema[:id].source
    #   # => :tasks
    #
    #   Users.schema[:user_id].source
    #   # => :tasks
    #
    # @return [Symbol, Relation::Name]
    #
    # @api public
    def source = meta[:source]

    # Return target relation of this attribute type
    #
    # @example
    #   class Tasks < ROM::Relation[:memory]
    #     schema do
    #       attribute :id, Types::Integer
    #       attribute :user_id, Types.ForeignKey(:users)
    #     end
    #   end
    #
    #   Users.schema[:id].target
    #   # => nil
    #
    #   Users.schema[:user_id].target
    #   # => :users
    #
    # @return [NilClass, Symbol, Relation::Name]
    #
    # @api public
    def target = meta[:target]

    # Return tuple key
    #
    # When schemas are projected with aliased attributes, we need a simple access to tuple keys
    #
    # @example
    #   class Tasks < ROM::Relation[:memory]
    #     schema do
    #       attribute :user_id, Types::Integer, alias: :id
    #       attribute :name, Types::String
    #     end
    #   end
    #
    #   Users.schema[:id].key
    #   # :id
    #
    #   Users.schema.project(Users.schema[:id].aliased(:user_id)).key
    #   # :user_id
    #
    # @return [Symbol]
    #
    # @api public
    def key = self.alias || name

    # Return new attribute type with provided alias
    #
    # @example
    #   class Tasks < ROM::Relation[:memory]
    #     schema do
    #       attribute :user_id, Types::Integer
    #       attribute :name, Types::String
    #     end
    #   end
    #
    #   aliased_user_id = Users.schema[:user_id].aliased(:id)
    #
    #   aliased_user_id.aliased?
    #   # => true
    #
    #   aliased_user_id.name
    #   # => :user_id
    #
    #   aliased_user_id.alias
    #   # => :id
    #
    # @param [Symbol] name The alias
    #
    # @return [Attribute]
    #
    # @api public
    def aliased(name) = with(alias: name)
    alias_method :as, :aliased

    # Return new attribute type with an alias using provided prefix
    #
    # @example
    #   class Users < ROM::Relation[:memory]
    #     schema do
    #       attribute :id, Types::Integer
    #       attribute :name, Types::String
    #     end
    #   end
    #
    #   prefixed_id = Users.schema[:id].prefixed
    #
    #   prefixed_id.aliased?
    #   # => true
    #
    #   prefixed_id.name
    #   # => :id
    #
    #   prefixed_id.alias
    #   # => :users_id
    #
    #   prefixed_id = Users.schema[:id].prefixed(:user)
    #
    #   prefixed_id.alias
    #   # => :user_id
    #
    # @param [Symbol] prefix The prefix (defaults to source.dataset)
    #
    # @return [Attribute]
    #
    # @api public
    def prefixed(prefix = source.dataset)
      aliased(:"#{prefix}_#{name}")
    end

    # Return if the attribute type is from a wrapped relation
    #
    # Wrapped attributes are used when two schemas from different relations
    # are merged together. This way we can identify them easily and handle
    # correctly in places like auto-mapping.
    #
    # @api public
    def wrapped? = meta[:wrapped].equal?(true)

    # Return attribute type wrapped for the specified relation name
    #
    # @param [Symbol] name The name of the source relation (defaults to source.dataset)
    #
    # @return [Attribute]
    #
    # @api public
    def wrapped(name = source.dataset)
      prefixed(name).meta(wrapped: true)
    end

    # Return attribute type with additional meta information
    #
    # Return meta information hash if no opts are provided
    #
    # @param [Hash] opts The meta options
    #
    # @return [Attribute]
    #
    # @api public
    def meta(opts = nil)
      if opts
        self.class.new(type.meta(opts), **options)
      else
        type.meta
      end
    end

    # Return string representation of the attribute type
    #
    # @return [String]
    #
    # @api public
    def inspect
      opts = options.reject { |k| %i[type name].include?(k) }
      meta_and_opts = meta.merge(opts).map { |k, v| "#{k}=#{v.inspect}" }
      %(#<#{self.class}[#{type.name}] name=#{name.inspect} #{meta_and_opts.join(' ')}>)
    end
    alias_method :pretty_inspect, :inspect

    # Check if the attribute type is equal to another
    #
    # @param [Dry::Type, Attribute] other
    #
    # @return [TrueClass,FalseClass]
    #
    # @api public
    def eql?(other)
      other.is_a?(self.class) ? super : type.eql?(other)
    end

    # Return if this attribute type has additional attribute type for reading
    # tuple values
    #
    # @return [TrueClass, FalseClass]
    #
    # @api private
    def read? = !meta[:read].nil?

    # Return read type
    #
    # @return [Dry::Types::Type]
    #
    # @api private
    def to_read_type = read? ? meta[:read] : type

    # Return write type
    #
    # @return [Dry::Types::Type]
    #
    # @api private
    def to_write_type = type

    # Return nullable attribute
    #
    # @return [Attribute]
    #
    # @api public
    def optional
      sum = self.class.new(super, **options)
      if read?
        sum.meta(read: meta[:read].optional)
      else
        sum
      end
    end

    # @api private
    def respond_to_missing?(name, include_private = false)
      type.respond_to?(name) || super
    end

    # Return AST for the type
    #
    # @return [Array]
    #
    # @api public
    def to_ast
      [:attribute, [name, type.to_ast(meta: false), meta_options_ast]]
    end

    # Return AST for the read type
    #
    # @return [Array]
    #
    # @api public
    def to_read_ast
      [:attribute, [name, to_read_type.to_ast(meta: false), meta_options_ast]]
    end

    # @api private
    def meta_options_ast
      keys = %i[wrapped primary_key alias]
      ast = meta.merge(options).select { |k, _| keys.include?(k) }
      ast[:source] = source.to_sym if source
      ast
    end

    memoize :to_ast, :to_read_ast, :meta_options_ast

    private

    # @api private
    def method_missing(meth, ...)
      if type.respond_to?(meth)
        response = type.__send__(meth, ...)

        if response.is_a?(type.class)
          self.class.new(response, **options)
        else
          response
        end
      else
        super
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/constants'
require 'rom/attribute'
require 'rom/schema/dsl'
require 'rom/schema/inferrer'
require 'rom/association_set'
require 'rom/support/notifications'
require 'rom/support/memoizable'

module ROM
  # Relation schema
  #
  # Schemas hold detailed information about relation tuples, including their
  # primitive types (String, Integer, Hash, etc. or custom classes), as well as
  # various meta information like primary/foreign key and literally any other
  # information that a given database adapter may need.
  #
  # Adapters can extend this class and it can be used in adapter-specific relations.
  # In example rom-sql extends schema with Association DSL and many additional
  # SQL-specific APIs in schema types.
  #
  # Schemas are used for projecting canonical relations into other relations and
  # every relation object maintains its schema. This means that we always have
  # all information about relation tuples, even when a relation was projected and
  # diverged from its canonical form.
  #
  # Furthermore schema attributes know their source relations, which makes it
  # possible to merge schemas from multiple relations and maintain information
  # about the source relations. In example when two relations are joined, their
  # schemas are merged, and we know which attributes belong to which relation.
  #
  # @api public
  # rubocop:disable Metrics/ClassLength
  class Schema
    include Memoizable

    extend Notifications::Listener

    subscribe('configuration.relations.registry.created') do |event|
      registry = event[:registry]

      registry.each_value do |relation|
        unless relation.schema.frozen?
          relation.schema.finalize_associations!(relations: registry)
          relation.schema.finalize!
        end
      end
    end

    subscribe('configuration.relations.schema.allocated') do |event|
      schema = event[:schema]
      registry = event[:registry]
      gateway = event[:gateway]

      unless schema.frozen?
        schema.finalize_attributes!(gateway: gateway, relations: registry)
        schema.set!(:relations, registry)
      end
    end

    EMPTY_ASSOCIATION_SET = AssociationSet.build(EMPTY_HASH).freeze

    DEFAULT_INFERRER = Inferrer.new(enabled: false).freeze

    type_transformation = lambda do |key|
      k = if key.default?
            key.constructor { |value| value.nil? ? Undefined : value }
          else
            key
          end
      k.required(false)
    end

    HASH_SCHEMA = Types::Coercible::Hash
      .schema(EMPTY_HASH)
      .with_type_transform(type_transformation)

    extend Initializer

    include Dry::Equalizer(:name, :attributes, :associations)
    include Enumerable

    # @!attribute [r] name
    #   @return [Symbol] The name of this schema
    param :name

    # @!attribute [r] attributes
    #   @return [Array] Array with schema attributes
    option :attributes, default: -> { EMPTY_ARRAY }

    # @!attribute [r] associations
    #   @return [AssociationSet] Optional association set (this is adapter-specific)
    option :associations, default: -> { EMPTY_ASSOCIATION_SET }

    # @!attribute [r] inferrer
    #   @return [#call] An optional inferrer object used in `finalize!`
    option :inferrer, default: -> { DEFAULT_INFERRER }

    # @api private
    option :relations, default: -> { EMPTY_HASH }

    # @!attribute [r] canonical
    #   @return [Symbol] The canonical schema which is carried in all schema instances
    option :canonical, default: -> { self }

    # @api private
    option :attr_class, default: -> { Attribute }

    # @!attribute [r] primary_key_name
    #   @return [Symbol] The name of the primary key. This is set because in
    #                    most of the cases relations don't have composite pks
    option :primary_key_name, optional: true

    # @!attribute [r] primary_key_names
    #   @return [Array<Symbol>] A list of all pk names
    option :primary_key_names, optional: true

    alias_method :to_ary, :attributes

    # Define a relation schema from plain rom types and optional options
    #
    # Resulting schema will decorate plain rom types with adapter-specific types
    # By default `Attribute` will be used
    #
    # @param [Relation::Name, Symbol] name The schema name, typically ROM::Relation::Name
    #
    # @return [Schema]
    #
    # @api public
    def self.define(name, attributes: EMPTY_ARRAY, attr_class: Attribute, **options)
      new(
        name,
        attr_class: attr_class,
        attributes: attributes(attributes, attr_class),
        **options
      ) { |schema| yield(schema) if block_given? }
    end

    # Builds a representation of the information needed to create an
    # attribute.
    #
    # This representation is consumed by `Schema.define` in order to create
    # the actual attributes.
    #
    # @return [Hash] A hash with `:type` and `:options` keys.
    #
    # @api private
    def self.build_attribute_info(type, **options)
      {
        type: type,
        options: options
      }
    end

    # @api private
    def self.attributes(attributes, attr_class)
      attributes.map do |attr|
        attr_class.new(attr[:type], **attr.fetch(:options))
      end
    end

    # @api private
    def initialize(*, **)
      super

      yield(self) if block_given?
    end

    # Abstract method for creating a new relation based on schema definition
    #
    # This can be used by views to generate a new relation automatically.
    # In example a schema can project a relation, join any additional relations
    # if it includes attributes from other relations etc.
    #
    # Default implementation is a no-op and it simply returns back untouched relation
    #
    # @param [Relation] relation
    #
    # @return [Relation]
    #
    # @api public
    def call(relation) = relation

    # Iterate over schema's attributes
    #
    # @yield [Attribute]
    #
    # @api public
    def each(&) = attributes.each(&)

    # Check if schema has any attributes
    #
    # @return [TrueClass, FalseClass]
    #
    # @api public
    def empty? = attributes.empty?

    # Coerce schema into a <AttributeName=>Attribute> Hash
    #
    # @return [Hash]
    #
    # @api public
    def to_h
      if block_given?
        super
      else
        super { [_1.name, _1] }
      end
    end

    # Return attribute
    #
    # @param [Symbol] key The attribute name
    # @param [Symbol, Relation::Name] src The source relation (for merged schemas)
    #
    # @raise KeyError
    #
    # @api public
    def [](key, src = name.to_sym)
      attr =
        if count_index[key].equal?(1)
          name_index[key]
        else
          source_index[src][key]
        end

      raise(KeyError, "#{key.inspect} attribute doesn't exist in #{src} schema") unless attr

      attr
    end

    # Project a schema to include only specified attributes
    #
    # @param [*Array<Symbol, Attribute>] names Attribute names
    #
    # @return [Schema]
    #
    # @api public
    def project(*names)
      new(names.map { |name| name.is_a?(Symbol) ? self[name] : name })
    end

    # Exclude provided attributes from a schema
    #
    # @param [*Array] names Attribute names
    #
    # @return [Schema]
    #
    # @api public
    def exclude(*names) = project(*(map(&:name) - names))

    # Project a schema with renamed attributes
    #
    # @param [Hash] mapping The attribute mappings
    #
    # @return [Schema]
    #
    # @api public
    def rename(mapping)
      new_attributes = map do |attr|
        alias_name = mapping[attr.name]
        alias_name ? attr.aliased(alias_name) : attr
      end

      new(new_attributes)
    end

    # Project a schema with renamed attributes using provided prefix
    #
    # @param [Symbol] prefix The name of the prefix
    #
    # @return [Schema]
    #
    # @api public
    def prefix(prefix)
      new(map { |attr| attr.prefixed(prefix) })
    end

    # Return new schema with all attributes marked as prefixed and wrapped
    #
    # This is useful when relations are joined and the right side should be marked
    # as wrapped
    #
    # @param [Symbol] prefix The prefix used for aliasing wrapped attributes
    #
    # @return [Schema]
    #
    # @api public
    def wrap(prefix = name.dataset)
      new(map { |attr| attr.wrapped? ? attr : attr.wrapped(prefix) })
    end

    # Return FK attribute for a given relation name
    #
    # @return [Attribute]
    #
    # @api public
    def foreign_key(relation)
      detect { |attr| attr.foreign_key? && attr.target == relation }
    end

    # Return primary key attributes
    #
    # @return [Array<Attribute>]
    #
    # @api public
    def primary_key = select(&:primary_key?)

    # Merge with another schema
    #
    # @param [Schema] other Other schema
    #
    # @return [Schema]
    #
    # @api public
    def merge(other) = append(*other)
    alias_method :+, :merge

    # Append more attributes to the schema
    #
    # This returns a new schema instance
    #
    # @param [Array<Attribute>] new_attributes
    #
    # @return [Schema]
    #
    # @api public
    def append(*new_attributes)
      new(attributes + new_attributes)
    end

    # Return a new schema with uniq attributes
    #
    # @return [Schema]
    #
    # @api public
    def uniq(&)
      if block_given?
        new(attributes.uniq(&))
      else
        new(attributes.uniq(&:name))
      end
    end

    # Return if a schema includes an attribute with the given name
    #
    # @param [Symbol] name The name of the attribute
    #
    # @return [Boolean]
    #
    # @api public
    def key?(name)
      !attributes.detect { |attr| attr.name == name }.nil?
    end

    # Return if a schema is canonical
    #
    # @return [Boolean]
    #
    # @api public
    def canonical? = equal?(canonical)

    # Finalize a schema
    #
    # @return [self]
    #
    # @api private
    def finalize!(**_opts)
      return self if frozen?

      freeze
    end

    # This hook is called when relation is being build during container finalization
    #
    # When block is provided it'll be called just before freezing the instance
    # so that additional ivars can be set
    #
    # @return [self]
    #
    # @api private
    def finalize_attributes!(gateway: nil, **)
      inferrer.(self, gateway).each { |key, value| set!(key, value) }

      yield if block_given?

      initialize_primary_key_names

      self
    end

    # Finalize associations defined in a schema
    #
    # @param [RelationRegistry] relations
    #
    # @return [self]
    #
    # @api private
    def finalize_associations!(**)
      set!(:associations, yield) if associations.any?
      self
    end

    # Return coercion function using attribute read types
    #
    # This is used for `output_schema` in relations
    #
    # @return [Dry::Types::Hash]
    #
    # @api private
    def to_output_hash
      HASH_SCHEMA.schema(
        to_h { |attr| [attr.key, attr.to_read_type] }
      )
    end

    # Return coercion function using attribute types
    #
    # This is used for `input_schema` in relations, typically commands use it
    # for processing input
    #
    # @return [Dry::Types::Hash]
    #
    # @api private
    def to_input_hash
      HASH_SCHEMA.schema(
        to_h { |attr| [attr.name, attr.to_write_type] }
      )
    end

    # Return AST for the schema
    #
    # @return [Array]
    #
    # @api public
    def to_ast = [:schema, [name, attributes.map(&:to_ast)]]

    # @api private
    def set!(key, value)
      instance_variable_set("@#{key}", value)
      options[key] = value
    end

    private

    # @api private
    def count_index
      to_h { |attr| [attr.name, count { |a| a.name.eql?(attr.name) }] }
    end

    # @api private
    def name_index = to_h { |attr| [attr.name, attr] }

    # @api private
    def source_index
      select(&:source)
        .group_by(&:source)
        .to_h { |src, grp| [src.to_sym, grp.to_h { |attr| [attr.name, attr] }] }
    end

    # @api private
    def new(attributes)
      self.class.new(name, **options, attributes: attributes)
    end

    # @api private
    def initialize_primary_key_names
      return if primary_key.empty?

      set!(:primary_key_name, primary_key[0].name)
      set!(:primary_key_names, primary_key.map(&:name))
    end

    memoize :count_index, :name_index, :source_index, :to_ast, :to_input_hash, :to_output_hash
  end
  # rubocop:enable Metrics/ClassLength
end


# frozen_string_literal: true

require 'rom/configuration'
require 'rom/environment'
require 'rom/setup'
require 'rom/setup/finalize'

module ROM
  # @api private
  class CreateContainer
    # @api private
    attr_reader :container

    # @api private
    def initialize(environment, setup)
      @container = finalize(environment, setup)
    end

    private

    # @api private
    def finalize(environment, setup)
      environment.configure do |config|
        environment.gateways.each_key do |key|
          gateway_config = config.gateways[key]
          gateway_config.infer_relations = true unless gateway_config.key?(:infer_relations)
        end
      end

      finalize = Finalize.new(
        gateways: environment.gateways,
        relation_classes: setup.relation_classes,
        command_classes: setup.command_classes,
        mappers: setup.mapper_classes,
        plugins: setup.plugins,
        notifications: setup.notifications,
        config: environment.config.dup.freeze
      )

      finalize.run!
    end
  end

  # @api private
  class InlineCreateContainer < CreateContainer
    # @api private
    def initialize(*args, &)
      case args.first
      when Configuration
        environment = args.first.environment
        setup = args.first.setup
      when Environment
        environment = args.first
        setup = args[1]
      else
        configuration = Configuration.new(*args, &)
        environment = configuration.environment
        setup = configuration.setup
      end

      super(environment, setup)
    end
  end

  # @api private
  def self.container(...) = InlineCreateContainer.new(...).container
end

class InlineCreateContainer < < CreateContainer


# frozen_string_literal: true

module ROM
  # Data pipeline common interface
  #
  # @api private
  module Pipeline
    # Common `>>` operator extension
    #
    # @api private
    module Operator
      # Compose two relation with a left-to-right composition
      #
      # @example
      #   users.by_name('Jane') >> tasks.for_users
      #
      # @param [Relation] other The right relation
      #
      # @return [Relation::Composite]
      #
      # @api public
      def >>(other) = composite_class.new(self, other)

      private

      # @api private
      def composite_class = raise ::NotImplementedError
    end

    include Operator

    # Send data through specified mappers
    #
    # @return [Relation::Composite]
    #
    # @api public
    def map_with(*names)
      [self, *names.map { |name| mappers[name] }]
        .reduce { |a, e| composite_class.new(a, e) }
    end

    # Forwards messages to the left side of a pipeline
    #
    # @api private
    module Proxy
      # @api private
      def respond_to_missing?(name, include_private = false)
        left.respond_to?(name) || super
      end

      private

      # Check if response from method missing should be decorated
      #
      # @api private
      def decorate?(response) = response.is_a?(left.class)

      # @api private
      def method_missing(name, ...)
        if left.respond_to?(name)
          response = left.__send__(name, ...)

          if decorate?(response)
            self.class.new(response, right)
          else
            response
          end
        else
          super
        end
      end
    end

    # Base composite class with left-to-right pipeline behavior
    #
    # @api private
    class Composite < ::BasicObject
      %i[block_given? respond_to_missing?].each do |method|
        define_method(method, ::Kernel.instance_method(method))
        private method
      end

      %i[class to_s inspect is_a? instance_of? to_enum].each do |method|
        define_method(method, ::Kernel.instance_method(method))
      end

      include ::Dry::Equalizer(:left, :right)
      include ::ROM::Pipeline::Proxy

      # @api private
      attr_reader :left

      # @api private
      attr_reader :right

      # @api private
      def initialize(left, right)
        @left = left
        @right = right
      end

      # Compose this composite with another object
      #
      # @api public
      def >>(other) = self.class.new(self, other)
    end
  end
end


# frozen_string_literal: true

require 'rom/header/attribute'

module ROM
  # Header provides information about data mapping of a specific relation
  #
  # Processors use headers to build objects that process raw relations that go
  # through mappers.
  #
  # @private
  class Header
    include ::Enumerable
    include ::Dry::Equalizer(:attributes, :model)

    # @return [Class] optional model associated with a header
    #
    # @api private
    attr_reader :model

    # @api private
    attr_reader :reject_keys

    # @api private
    attr_reader :copy_keys

    # @api private
    attr_reader :attributes

    # @return [Hash] attribute key/name mapping for all primitive attributes
    #
    # @api private
    attr_reader :mapping

    # @return [Array] all attribute keys that are in a tuple
    #
    # @api private
    attr_reader :tuple_keys

    # @return [Array] all attribute names that are popping from a tuple
    #
    # @api private
    attr_reader :pop_keys

    # Coerce array with attribute definitions into a header object
    #
    # @param [Array<Array>] input attribute name/option pairs
    # @param [Hash] options
    #
    # @return [Header]
    #
    # @api private
    def self.coerce(input, options = {})
      if input.instance_of?(self)
        input
      else
        attributes = input.to_h { [_1.first, Attribute.coerce(_1)] }

        new(attributes, options)
      end
    end

    # @api private
    def initialize(attributes, options = {})
      @options = options
      @model = options[:model]
      @copy_keys = options.fetch(:copy_keys, false)
      @reject_keys = options.fetch(:reject_keys, false)

      @attributes = attributes
      initialize_mapping
      initialize_tuple_keys
      initialize_pop_keys
    end

    # Iterate over attributes
    #
    # @yield [Attribute]
    #
    # @api private
    def each(&) = attributes.each_value(&)

    # Return if there are any aliased attributes
    #
    # @api private
    def aliased? = any?(&:aliased?)

    # Return attribute keys
    #
    # An attribute key corresponds to tuple attribute names
    #
    # @api private
    def keys = attributes.keys

    # Return attribute identified by its name
    #
    # @return [Attribute]
    #
    # @api private
    def [](name) = attributes.fetch(name)

    # Return all Combined attributes
    #
    # @return [Array<Combined>]
    #
    # @api private
    def combined = by_type(Combined)

    # Returns all attributes that require preprocessing
    #
    # @return [Array<Group,Fold>]
    #
    # @api private
    def preprocessed = by_type(Group, Fold)

    # Returns all attributes that require postprocessing
    #
    # @return [Array<Ungroup,Unfold>]
    #
    # @api private
    def postprocessed = by_type(Ungroup, Unfold)

    # Return all Wrap attributes
    #
    # @return [Array<Wrap>]
    #
    # @api private
    def wraps = by_type(Wrap)

    # Return all non-primitive attributes that don't require mapping
    #
    # @return [Array<Group,Fold,Ungroup,Unfold,Wrap,Unwrap>]
    #
    # @api private
    def non_primitives = preprocessed + wraps

    # Return all primitive attributes that require mapping
    #
    # @return [Array<Attribute>]
    #
    # @api private
    def primitives = to_a - non_primitives

    private

    # Find all attribute matching specific attribute class (not kind)
    #
    # @return [Array<Attribute>]
    #
    # @api private
    def by_type(*types)
      select { |attribute| types.include?(attribute.class) }
    end

    # Set mapping hash from primitive attributes
    #
    # @api private
    def initialize_mapping
      @mapping = primitives.map(&:mapping).reduce(:merge) || {}
    end

    # Set all tuple keys from all attributes going deep into Wrap and Group too
    #
    # @api private
    def initialize_tuple_keys
      @tuple_keys = mapping.keys.flatten + non_primitives.flat_map(&:tuple_keys)
    end

    # Set all tuple keys from all attributes popping from Unwrap and Ungroup
    #
    # @api private
    def initialize_pop_keys
      @pop_keys = mapping.values + non_primitives.flat_map(&:tuple_keys)
    end
  end
end


# frozen_string_literal: true

require 'rom/support/inflector'

require 'rom/initializer'
require 'rom/commands'
require 'rom/command_proxy'

module ROM
  # Builds commands for relations.
  #
  # This class is used by repositories to automatically create commands for
  # their relations. This is used both by `Repository#command` method and
  # `commands` repository class macros.
  #
  # @api private
  class CommandCompiler
    extend Initializer

    # @api private
    def self.registry
      Hash.new { |h, k| h[k] = {} }
    end

    # @!attribute [r] gateways
    #   @return [ROM::Registry] Gateways used for command extensions
    param :gateways

    # @!attribute [r] relations
    #   @return [ROM::RelationRegistry] Relations used with a given compiler
    param :relations

    # @!attribute [r] commands
    #   @return [ROM::Registry] Command registries with custom commands
    param :commands

    # @!attribute [r] notifications
    #   @return [Notifications::EventBus] Configuration notifications event bus
    param :notifications

    # @!attribute [r] id
    #   @return [Symbol] The command type registry identifier
    option :id, optional: true

    # @!attribute [r] adapter
    #   @return [Symbol] The adapter identifier ie :sql or :http
    option :adapter, optional: true

    # @!attribute [r] registry
    #   @return [Hash] local registry where commands will be stored during compilation
    option :registry, optional: true, default: -> { self.class.registry }

    # @!attribute [r] plugins
    #   @return [Array<Symbol>] a list of optional plugins that will be enabled for commands
    option :plugins, optional: true, default: -> { EMPTY_ARRAY }

    # @!attribute [r] plugins_options
    #   @return [Hash] a hash of options for the plugins
    option :plugins_options, optional: true, default: -> { EMPTY_HASH }

    # @!attribute [r] meta
    #   @return [Array<Symbol>] Meta data for a command
    option :meta, optional: true

    # @!attribute [r] cache
    #   @return [Cache] local cache instance
    option :cache, default: -> { Cache.new }

    # Return a specific command type for a given adapter and relation AST
    #
    # This class holds its own registry where all generated commands are being
    # stored
    #
    # CommandProxy is returned for complex command graphs as they expect root
    # relation name to be present in the input, which we don't want to have
    # in repositories. It might be worth looking into removing this requirement
    # from rom core Command::Graph API.
    #
    # @overload [](type, adapter, ast, plugins, meta)
    #   @param type [Symbol] The type of command
    #   @param adapter [Symbol] The adapter identifier
    #   @param ast [Array] The AST representation of a relation
    #   @param plugins [Array<Symbol>] A list of optional command plugins that should be used
    #   @param meta [Hash] Meta data for a command
    #
    #   @return [Command, CommandProxy]
    #
    # @api private
    def call(*args)
      cache.fetch_or_store(args.hash) do
        type, adapter, ast, plugins, plugins_options, meta = args

        compiler = with(
          id: type,
          adapter: adapter,
          plugins: Array(plugins),
          plugins_options: plugins_options,
          meta: meta
        )

        graph_opts = compiler.visit(ast)
        command = ROM::Commands::Graph.build(registry, graph_opts)

        if command.graph?
          CommandProxy.new(command)
        elsif command.lazy?
          command.unwrap
        else
          command
        end
      end
    end
    alias_method :[], :call

    # @api private
    def type
      @_type ||= Commands.const_get(Inflector.classify(id))[adapter]
    rescue ::NameError
      nil
    end

    # @api private
    def visit(ast, *args)
      name, node = ast
      __send__(:"visit_#{name}", node, *args)
    end

    private

    # @api private
    #
    # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
    def visit_relation(node, parent_relation = nil)
      name, header, meta = node
      other = header.map { |attr| visit(attr, name) }.compact

      if type
        register_command(name, type, meta, parent_relation)

        default_mapping =
          if meta[:combine_type] == :many
            name
          else
            { Inflector.singularize(name).to_sym => name }
          end

        mapping =
          if parent_relation
            associations = relations[parent_relation].associations

            assoc = associations[meta[:combine_name]]

            if assoc
              { assoc.key => assoc.target.name.to_sym }
            else
              default_mapping
            end
          else
            default_mapping
          end

        if other.empty?
          [mapping, type]
        else
          [mapping, [type, other]]
        end
      else
        registry[name][id] = commands[name][id]
        [name, id]
      end
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

    # @api private
    def visit_attribute(*) = nil

    # Build a command object for a specific relation
    #
    # The command will be prepared for handling associations if it's a combined
    # relation. Additional plugins will be enabled if they are configured for
    # this compiler.
    #
    # @param [Symbol] rel_name A relation identifier from the container registry
    # @param [Symbol] type The command type
    # @param [Hash] rel_meta Meta information from relation AST
    # @param [Symbol] parent_relation Optional parent relation identifier
    #
    # @return [ROM::Command]
    #
    # @api private
    #
    # rubocop:disable Metrics/AbcSize
    def register_command(rel_name, type, rel_meta, parent_relation = nil)
      relation = relations[rel_name]

      type.create_class(rel_name, type) do |klass|
        klass.result(rel_meta.fetch(:combine_type, result))

        meta.each do |name, value|
          klass.public_send(name, value)
        end

        setup_associates(klass, relation, rel_meta, parent_relation) if rel_meta[:combine_type]

        plugins.each do |plugin|
          plugin_options = plugins_options.fetch(plugin) { EMPTY_HASH }
          klass.use(plugin, **plugin_options)
        end

        gateway = gateways[relation.gateway]

        notifications.trigger(
          'configuration.commands.class.before_build',
          command: klass, gateway: gateway, dataset: relation.dataset, adapter: adapter
        )

        klass.extend_for_relation(relation) if klass.restrictable

        registry[rel_name][type] = klass.build(relation)
      end
    end
    # rubocop:enable Metrics/AbcSize

    # Return default result type
    #
    # @return [Symbol]
    #
    # @api private
    def result = meta.fetch(:result, :one)

    # Sets up `associates` plugin for a given command class and relation
    #
    # @param [Class] klass The command class
    # @param [Relation] relation The relation for the command
    #
    # @api private
    def setup_associates(klass, relation, _meta, parent_relation)
      assoc_name =
        if relation.associations.key?(parent_relation)
          parent_relation
        else
          singular_name = Inflector.singularize(parent_relation).to_sym
          singular_name if relation.associations.key?(singular_name)
        end

      klass.associates(assoc_name || parent_relation)
    end
  end
end

call

    def fetch_or_store(*args, &) = objects.fetch_or_store(args.hash, &)

    # @api private
    def size = objects.size

    # @api private
    def namespaced(namespace)
      @namespaced[namespace] ||= Namespaced.new(objects, namespace)
    end

build

        def build(registry, options, path = EMPTY_ARRAY)
          options.reduce { |spec, other| build_command(registry, spec, other, path) }
        end


def visit_relation(node, parent_relation = nil)
      name, header, meta = node
      other = header.map { |attr| visit(attr, name) }.compact

      if type
        register_command(name, type, meta, parent_relation)

        default_mapping =
          if meta[:combine_type] == :many
            name
          else
            { Inflector.singularize(name).to_sym => name }
          end

        mapping =
          if parent_relation
            associations = relations[parent_relation].associations

            assoc = associations[meta[:combine_name]]

            if assoc
              { assoc.key => assoc.target.name.to_sym }
            else
              default_mapping
            end
          else
            default_mapping
          end

        if other.empty?
          [mapping, type]
        else
          [mapping, [type, other]]
        end
      else
        registry[name][id] = commands[name][id]
        [name, id]
      end
    end

visit

    def visit(ast, *args)
      name, node = ast
      __send__(:"visit_#{name}", node, *args)
    end

    def register_command(rel_name, type, rel_meta, parent_relation = nil)
      relation = relations[rel_name]

      type.create_class(rel_name, type) do |klass|
        klass.result(rel_meta.fetch(:combine_type, result))

        meta.each do |name, value|
          klass.public_send(name, value)
        end

        setup_associates(klass, relation, rel_meta, parent_relation) if rel_meta[:combine_type]

        plugins.each do |plugin|
          plugin_options = plugins_options.fetch(plugin) { EMPTY_HASH }
          klass.use(plugin, **plugin_options)
        end

        gateway = gateways[relation.gateway]

        notifications.trigger(
          'configuration.commands.class.before_build',
          command: klass, gateway: gateway, dataset: relation.dataset, adapter: adapter
        )

        klass.extend_for_relation(relation) if klass.restrictable

        registry[rel_name][type] = klass.build(relation)
      end
    end


def register_command(rel_name, type, rel_meta, parent_relation = nil)
      relation = relations[rel_name]

      type.create_class(rel_name, type) do |klass|
        klass.result(rel_meta.fetch(:combine_type, result))

        meta.each do |name, value|
          klass.public_send(name, value)
        end

        setup_associates(klass, relation, rel_meta, parent_relation) if rel_meta[:combine_type]

        plugins.each do |plugin|
          plugin_options = plugins_options.fetch(plugin) { EMPTY_HASH }
          klass.use(plugin, **plugin_options)
        end

        gateway = gateways[relation.gateway]

        notifications.trigger(
          'configuration.commands.class.before_build',
          command: klass, gateway: gateway, dataset: relation.dataset, adapter: adapter
        )

        klass.extend_for_relation(relation) if klass.restrictable

        registry[rel_name][type] = klass.build(relation)
      end
    end

    def setup_associates(klass, relation, _meta, parent_relation)
      assoc_name =
        if relation.associations.key?(parent_relation)
          parent_relation
        else
          singular_name = Inflector.singularize(parent_relation).to_sym
          singular_name if relation.associations.key?(singular_name)
        end

      klass.associates(assoc_name || parent_relation)
    end

trigger

      def trigger(event_id, payload = EMPTY_HASH)
        event = events[event_id]

        listeners[event.id].each do |(listener, query)|
          event.payload(payload).trigger(listener, query)
        end
      end


def setup_associates(klass, relation, _meta, parent_relation)
      assoc_name =
        if relation.associations.key?(parent_relation)
          parent_relation
        else
          singular_name = Inflector.singularize(parent_relation).to_sym
          singular_name if relation.associations.key?(singular_name)
        end

      klass.associates(assoc_name || parent_relation)
    end

key?

    def key?(name) = !name.nil? && elements.key?(name.to_sym)

    # @api private
    def fetch(key)
      raise ::ArgumentError, 'key cannot be nil' if key.nil?

      elements.fetch(key.to_sym) do
        return yield if block_given?

        raise self.class.element_not_found_error.new(key, self)
      end
    end


# frozen_string_literal: true

require 'rom/transaction'
require 'rom/support/notifications'

module ROM
  # Abstract gateway class
  #
  # Every adapter needs to inherit from this class and implement
  # required interface
  #
  # @abstract
  #
  # @api public
  class Gateway
    extend Dry::Core::ClassAttributes
    extend Notifications::Listener

    # @!method self.adapter
    #  Get or set gateway's adapter identifier
    #
    #  @overload adapter
    #    Return adapter identifier
    #    @return [Symbol]
    #
    #  @overload gateway(adapter)
    #    @example
    #      class MyGateway < ROM::Gateway
    #        adapter :my_adapter
    #      end
    #
    #    @param [Symbol] adapter The adapter identifier
    defines :adapter

    # @!attribute [r] connection
    #   @return [Object] The gateway's connection object (type varies across adapters)
    attr_reader :connection

    # Set up a gateway
    #
    # @overload setup(type, *args)
    #   Sets up a single-gateway given a gateway type.
    #   For custom gateways, create an instance and pass it directly.
    #
    #   @example
    #     module SuperDB
    #       class Gateway < ROM::Gateway
    #         def initialize(options)
    #         end
    #       end
    #     end
    #
    #     ROM.register_adapter(:super_db, SuperDB)
    #
    #     Gateway.setup(:super_db, some: 'options')
    #     # SuperDB::Gateway.new(some: 'options') is called
    #
    #   @param [Symbol] type Registered gateway identifier
    #   @param [Array] args Additional gateway options
    #
    # @overload setup(gateway)
    #   Set up a gateway instance
    #
    #   @example
    #     module SuperDB
    #       class Gateway < ROM::Gateway
    #         def initialize(options)
    #         end
    #       end
    #     end
    #
    #     ROM.register_adapter(:super_db, SuperDB)
    #
    #     Gateway.setup(SuperDB::Gateway.new(some: 'options'))
    #
    #   @param [Gateway] gateway
    #
    # @return [Gateway] a specific gateway subclass
    #
    # @api public
    def self.setup(gateway_or_scheme, *args, **kwargs)
      case gateway_or_scheme
      when String
        raise ArgumentError, <<-STRING.gsub(/^ {10}/, '')
          URIs without an explicit scheme are not supported anymore.
          See https://github.com/rom-rb/rom/blob/master/CHANGELOG.md
        STRING
      when Symbol
        klass = class_from_symbol(gateway_or_scheme)

        if klass.instance_method(:initialize).arity.zero?
          klass.new
        else
          klass.new(*args, **kwargs)
        end
      else
        raise ArgumentError, "Can't accept arguments when passing an instance" unless args.empty?

        gateway_or_scheme
      end
    end

    # Get gateway subclass for a specific adapter
    #
    # @param [Symbol] type Adapter identifier
    #
    # @return [Class]
    #
    # @api private
    def self.class_from_symbol(type)
      adapter = ROM.adapters.fetch(type) do
        begin
          require "rom/#{type}"
        rescue LoadError
          raise AdapterLoadError, "Failed to load adapter rom/#{type}"
        end

        ROM.adapters.fetch(type)
      end

      adapter.const_get(:Gateway)
    end

    # Returns the adapter, defined for the class
    #
    # @return [Symbol]
    #
    # @api public
    def adapter
      self.class.adapter || raise(
        MissingAdapterIdentifierError,
        "gateway class +#{self}+ is missing the adapter identifier"
      )
    end

    # A generic interface for setting up a logger
    #
    # This is not a required interface, it's a no-op by default
    #
    # @abstract
    #
    # @api public
    def use_logger(*)
      # noop
    end

    # A generic interface for returning default logger
    #
    # Adapters should implement this method as handling loggers is different
    # across adapters. This is a no-op by default and returns nil.
    #
    # @return [NilClass]
    #
    # @api public
    def logger
      # noop
    end

    # Disconnect is optional and it's a no-op by default
    #
    # @api public
    def disconnect
      # noop
    end

    # Runs a block inside a transaction. The underlying transaction engine
    # is adapter-specific
    #
    # @param [Hash] opts Transaction options
    #
    # @return The result of yielding the block or +nil+ if
    #         the transaction was rolled back
    #
    # @api public
    def transaction(opts = EMPTY_HASH, &) = transaction_runner(opts).run(opts, &)

    private

    # @api private
    def transaction_runner(_) = Transaction::NoOp
  end
end


def self.class_from_symbol(type)
      adapter = ROM.adapters.fetch(type) do
        begin
          require "rom/#{type}"
        rescue LoadError
          raise AdapterLoadError, "Failed to load adapter rom/#{type}"
        end

        ROM.adapters.fetch(type)
      end

      adapter.const_get(:Gateway)
    end


# frozen_string_literal: true

require 'rom/struct'
require 'rom/constants'
require 'rom/initializer'
require 'rom/support/memoizable'

require 'rom/relation/class_interface'

require 'rom/auto_curry'
require 'rom/pipeline'
require 'rom/mapper_registry'
require 'rom/command_registry'

require 'rom/relation/loaded'
require 'rom/relation/curried'
require 'rom/relation/commands'
require 'rom/relation/composite'
require 'rom/relation/combined'
require 'rom/relation/wrap'
require 'rom/relation/materializable'
require 'rom/association_set'

require 'rom/types'
require 'rom/schema'

module ROM
  # Base relation class
  #
  # Relation is a proxy for the dataset object provided by the gateway. It
  # can forward methods to the dataset, which is why the "native" interface of
  # the underlying gateway is available in the relation
  #
  # Individual adapters sets up their relation classes and provide different APIs
  # depending on their persistence backend.
  #
  # @api public
  class Relation
    # Default no-op output schema which is called in `Relation#each`
    NOOP_OUTPUT_SCHEMA = -> tuple { tuple }.freeze

    extend Initializer
    extend ClassInterface

    include Relation::Commands
    include Memoizable

    extend Dry::Core::ClassAttributes

    defines :adapter, :schema_opts, :schema_class,
            :schema_attr_class, :schema_inferrer, :schema_dsl,
            :wrap_class

    # @!method self.gateway
    #  Manage the gateway
    #
    #  @overload gateway
    #    Return the gateway key that the relation is associated with
    #    @return [Symbol]
    #
    #  @overload gateway(gateway_key)
    #    Link the relation to a gateway. Change this setting if the
    #    relation is defined on a non-default gateway
    #
    #    @example
    #      class Users < ROM::Relation[:sql]
    #        gateway :custom
    #      end
    #
    #    @param [Symbol] gateway_key
    defines :gateway

    # @!method self.auto_map
    #   Whether or not a relation and its compositions should be auto-mapped
    #
    #   @overload auto_map
    #     Return auto_map setting value
    #     @return [Boolean]
    #
    #   @overload auto_map(value)
    #     Set auto_map value
    defines :auto_map

    # @!method self.auto_struct
    #   Whether or not tuples should be auto-mapped to structs
    #
    #   @overload auto_struct
    #     Return auto_struct setting value
    #     @return [Boolean]
    #
    #   @overload auto_struct(value)
    #     Set auto_struct value
    defines :auto_struct

    # @!method self.struct_namespace
    #  Get or set a namespace for auto-generated struct classes.
    #  By default, new struct classes are created within ROM::Struct
    #
    #   @example using custom namespace
    #     class Users < ROM::Relation[:sql]
    #       struct_namespace Entities
    #     end
    #
    #     users.by_pk(1).one! # => #<Entities::User id=1 name="Jane Doe">
    #
    #  @overload struct_namespace
    #    @return [Module] Default struct namespace
    #
    #  @overload struct_namespace(namespace)
    #    @param [Module] namespace
    #
    defines :struct_namespace

    gateway :default

    auto_map true
    auto_struct false
    struct_namespace ROM::Struct

    schema_opts EMPTY_HASH
    schema_dsl Schema::DSL
    schema_attr_class Attribute
    schema_class Schema
    schema_inferrer Schema::DEFAULT_INFERRER

    wrap_class Relation::Wrap

    include Dry::Equalizer(:name, :dataset)
    include Materializable
    include Pipeline

    # @!attribute [r] dataset
    #   @return [Object] dataset used by the relation provided by relation's gateway
    #   @api public
    param :dataset

    # @!attribute [r] schema
    #   @return [Schema] relation schema, defaults to class-level canonical
    #                    schema (if it was defined) and sets an empty one as
    #                    the fallback
    #   @api public
    option :schema, default: -> { self.class.schema || self.class.default_schema }

    # @!attribute [r] name
    #   @return [Object] The relation name
    #   @api public
    option :name, default: lambda {
      self.class.schema ? self.class.schema.name : self.class.default_name
    }

    # @!attribute [r] input_schema
    #   @return [Object#[]] tuple processing function, uses schema or defaults to Hash[]
    #   @api private
    option :input_schema, default: -> { schema.to_input_hash }

    # @!attribute [r] output_schema
    #   @return [Object#[]] tuple processing function, uses schema or defaults to NOOP_OUTPUT_SCHEMA
    #   @api private
    option :output_schema, default: lambda {
      schema.any?(&:read?) ? schema.to_output_hash : NOOP_OUTPUT_SCHEMA
    }

    # @!attribute [r] auto_map
    #   @return [true, false] Whether or not a relation and its
    #                         compositions should be auto-mapped
    #   @api private
    option :auto_map, default: -> { self.class.auto_map }

    # @!attribute [r] auto_struct
    #   @return [true, false] Whether or not tuples should be auto-mapped to structs
    #   @api private
    option :auto_struct, default: -> { self.class.auto_struct }

    # @!attribute [r] struct_namespace
    #   @return [Module] Custom struct namespace
    #   @api private
    option :struct_namespace, reader: false, default: -> { self.class.struct_namespace }

    # @!attribute [r] mappers
    #   @return [MapperRegistry] an optional mapper registry (empty by default)
    option :mappers, default: -> { self.class.mapper_registry }

    # @!attribute [r] commands
    #   @return [CommandRegistry] Command registry
    #   @api private
    option :commands, default: -> { CommandRegistry.new({}, relation_name: name.relation) }

    # @!attribute [r] meta
    #   @return [Hash] Meta data stored in a hash
    #   @api private
    option :meta, reader: true, default: -> { EMPTY_HASH }

    # Return schema attribute
    #
    # @example accessing canonical attribute
    #   users[:id]
    #   # => #<ROM::SQL::Attribute[Integer] primary_key=true
    #   #     name=:id source=ROM::Relation::Name(users)>
    #
    # @example accessing joined attribute
    #   tasks_with_users = tasks.join(users).select_append(tasks[:title])
    #   tasks_with_users[:title, :tasks]
    #   # => #<ROM::SQL::Attribute[String] primary_key=false
    #   #     name=:title source=ROM::Relation::Name(tasks)>
    #
    # @return [Attribute]
    #
    # @api public
    def [](name) = schema[name]

    # Yields relation tuples
    #
    # Every tuple is processed through Relation#output_schema, it's a no-op by default
    #
    # @yield [Hash]
    #
    # @return [Enumerator] if block is not provided
    #
    # @api public
    def each(&)
      return to_enum unless block_given?

      if auto_map?
        mapper.(dataset.map { |tuple| output_schema[tuple] }).each(&)
      else
        dataset.each { |tuple| yield(output_schema[tuple]) }
      end
    end

    # Combine with other relations using configured associations
    #
    # @overload combine(*associations)
    #   @example
    #     users.combine(:tasks, :posts)
    #
    #   @param *associations [Array<Symbol>] A list of association names
    #
    # @overload combine(*associations, **nested_associations)
    #   @example
    #     users.combine(:tasks, posts: :authors)
    #
    #   @param *associations [Array<Symbol>] A list of association names
    #   @param *nested_associations [Hash] A hash with nested association names
    #
    # @overload combine(associations)
    #   @example
    #     users.combine(posts: [:authors, reviews: [:tags, comments: :author])
    #
    #   @param *associations [Hash] A hash with nested association names
    #
    # @return [Relation]
    #
    # @api public
    def combine(*args) = combine_with(*nodes(*args))

    # Composes with other relations
    #
    # @param [Array<Relation>] others The other relation(s) to compose with
    #
    # @return [Relation::Graph]
    #
    # @api public
    def combine_with(*others) = Combined.new(self, others)

    # @api private
    def nodes(*args)
      args.reduce([]) do |acc, arg|
        case arg
        when Symbol
          acc << node(arg)
        when Hash
          acc.concat(arg.map { |name, opts| node(name).combine(opts) })
        when Array
          acc.concat(arg.map { |opts| nodes(opts) }.reduce(:concat))
        end
      end
    end

    # Create a graph node for a given association identifier
    #
    # @param [Symbol, Relation::Name] name
    #
    # @return [Relation]
    #
    # @api public
    def node(name)
      assoc = associations[name]
      other = assoc.node
      other.eager_load(assoc)
    end

    # Return a graph node prepared by the given association
    #
    # @param [Association] assoc An association object
    #
    # @return [Relation]
    #
    # @api public
    def eager_load(assoc)
      relation = assoc.prepare(self)

      if assoc.override?
        relation.(assoc)
      else
        relation.preload_assoc(assoc)
      end
    end

    # Preload other relation via association
    #
    # This is used internally when relations are composed
    #
    # @return [Relation::Curried]
    #
    # @api private
    def preload_assoc(assoc, other) = assoc.preload(self, other)

    # Wrap other relations using association names
    #
    # @example
    #   tasks.wrap(:owner)
    #
    # @param [Array<Symbol>] names A list with association identifiers
    #
    # @return [Wrap]
    #
    # @api public
    def wrap(*names)
      wrap_around(*names.map { |n| associations[n].wrap })
    end

    # Wrap around other relations
    #
    # @param [Array<Relation>] others Other relations
    #
    # @return [Relation::Wrap]
    #
    # @api public
    def wrap_around(*others) = wrap_class.new(self, others)

    # Loads a relation
    #
    # @return [Relation::Loaded]
    #
    # @api public
    def call = Loaded.new(self)

    # Materializes a relation into an array
    #
    # @return [Array<Hash>]
    #
    # @api public
    def to_a = to_enum.to_a

    # Returns if this relation is curried
    #
    # @return [false]
    #
    # @api private
    def curried? = false

    # Returns if this relation is a graph
    #
    # @return [false]
    #
    # @api private
    def graph? = false

    # Return if this is a wrap relation
    #
    # @return [false]
    #
    # @api private
    def wrap? = false

    # Returns true if a relation has schema defined
    #
    # @return [TrueClass, FalseClass]
    #
    # @api private
    def schema? = !schema.empty?

    # Return a new relation with provided dataset and additional options
    #
    # Use this method whenever you need to use dataset API to get a new dataset
    # and you want to return a relation back. Typically relation API should be
    # enough though. If you find yourself using this method, it might be worth
    # to consider reporting an issue that some dataset functionality is not available
    # through relation API.
    #
    # @example with a new dataset
    #   users.new(users.dataset.some_method)
    #
    # @example with a new dataset and options
    #   users.new(users.dataset.some_method, other: 'options')
    #
    # @param [Object] dataset
    # @param [Hash] new_opts Additional options
    #
    # @api public
    def new(dataset, **new_opts)
      opts =
        if new_opts.empty?
          options
        elsif new_opts.key?(:schema)
          options.merge(new_opts).except(:input_schema, :output_schema)
        else
          options.merge(new_opts)
        end

      self.class.new(dataset, **opts)
    end

    undef_method :with

    # Returns a new instance with the same dataset but new options
    #
    # @example
    #   users.with(output_schema: -> tuple { .. })
    #
    # @param [Hash] opts New options
    #
    # @return [Relation]
    #
    # @api public
    def with(opts)
      new_options =
        if opts.key?(:meta)
          opts.merge(meta: meta.merge(opts[:meta]))
        else
          opts
        end

      new(dataset, **options, **new_options)
    end

    # Return schema's association set (empty by default)
    #
    # @return [AssociationSet] Schema's association set (empty by default)
    #
    # @api public
    def associations = schema.associations

    # Returns AST for the wrapped relation
    #
    # @return [Array]
    #
    # @api public
    def to_ast = [:relation, [name.relation, attr_ast, meta_ast]]

    # @api private
    def attr_ast = schema.map(&:to_read_ast)

    # @api private
    def meta_ast
      meta = self.meta.merge(dataset: name.dataset, alias: name.aliaz,
                             struct_namespace: options[:struct_namespace])
      meta[:model] = false unless auto_struct? || meta[:model]
      meta
    end

    # @api private
    def auto_map? = (auto_map || auto_struct) && !meta[:combine_type]

    # @api private
    def auto_struct? = auto_struct && !meta[:combine_type]

    # @api private
    def mapper = mappers[to_ast]

    # Maps relation with custom mappers available in the registry
    #
    # When `auto_map` is enabled, your mappers will be applied after performing
    # default auto-mapping. This means that you can compose complex relations
    # and have them auto-mapped, and use much simpler custom mappers to adjust
    # resulting data according to your requirements.
    #
    # @overload map_with(*mappers)
    #   Map tuples using registered mappers
    #
    #   @example
    #     users.map_with(:my_mapper, :my_other_mapper)
    #
    #   @param [Array<Symbol>] mappers A list of mapper identifiers
    #
    # @overload map_with(*mappers, auto_map: true)
    #   Map tuples using custom registered mappers and enforce auto-mapping
    #
    #   @example
    #     users.map_with(:my_mapper, :my_other_mapper, auto_map: true)
    #
    #   @param [Array<Symbol>] mappers A list of mapper identifiers
    #
    # @return [Relation::Composite] Mapped relation
    #
    # @api public
    def map_with(*names, **opts) = super(*names).with(opts)

    # Return a new relation that will map its tuples to instances of the provided class
    #
    # @example
    #   users.map_to(MyUserModel)
    #
    # @param [Class] klass Your custom model class
    #
    # @return [Relation]
    #
    # @api public
    def map_to(klass, **opts)
      with(opts.merge(auto_map: false, auto_struct: true, meta: { model: klass }))
    end

    # Return a new relation with an aliased name
    #
    # @example
    #   users.as(:people)
    #
    # @param [Symbol] aliaz Aliased name
    #
    # @return [Relation]
    #
    # @api public
    def as(aliaz) = with(name: name.as(aliaz))

    # @return [Symbol] The wrapped relation's adapter identifier ie :sql or :http
    #
    # @api private
    def adapter = self.class.adapter

    # Return name of the source gateway of this relation
    #
    # @return [Symbol]
    #
    # @api private
    def gateway = self.class.gateway

    # Return all registered relation schemas
    #
    # This holds all schemas defined via `view` DSL
    #
    # @return [Hash<Symbol=>Schema>]
    #
    # @api public
    def schemas = self.class.schemas

    # Return a foreign key name for the provided relation name
    #
    # @param [Name] name The relation name object
    #
    # @return [Symbol]
    #
    # @api private
    def foreign_key(name)
      attr = schema.foreign_key(name.dataset)

      if attr
        attr.name
      else
        :"#{Inflector.singularize(name.dataset)}_id"
      end
    end

    # Return a new relation configured with the provided struct namespace
    #
    # @param [Module] ns Custom namespace module for auto-structs
    #
    # @return [Relation]
    #
    # @api public
    def struct_namespace(ns)
      options[:struct_namespace] == ns ? self : with(struct_namespace: ns)
    end

    memoize :to_ast, :auto_map?, :auto_struct?, :foreign_key, :combine, :wrap, :node

    # we do it here because we want to avoid previous methods to be auto_curried
    # via method_added hook, which is what AutoCurry uses
    extend AutoCurry

    auto_curry :preload_assoc

    private

    # Hook used by `Pipeline` to get the class that should be used for composition
    #
    # @return [Class]
    #
    # @api private
    def composite_class = Relation::Composite

    # Return configured "wrap" relation class used in Relation#wrap
    #
    # @return [Class]
    #
    # @api private
    def wrap_class = self.class.wrap_class
  end
end


def new(dataset, **new_opts)
      opts =
        if new_opts.empty?
          options
        elsif new_opts.key?(:schema)
          options.merge(new_opts).except(:input_schema, :output_schema)
        else
          options.merge(new_opts)
        end

      self.class.new(dataset, **opts)
    end


# frozen_string_literal: true

module ROM
  class Mapper
    # Setup DSL-specific mapper extensions
    #
    # @private
    class Builder
      # Generate a mapper subclass
      #
      # This is used by Setup#mappers DSL
      #
      # @api private
      def self.build_class(name, mapper_registry, options = EMPTY_HASH, &)
        class_name = "ROM::Mapper[#{name}]"

        parent = options[:parent]
        inherit_header = options.fetch(:inherit_header) { ROM::Mapper.inherit_header }

        parent_class =
          if parent
            mapper_registry.detect { |klass| klass.relation == parent }
          else
            ROM::Mapper
          end

        Dry::Core::ClassBuilder.new(name: class_name, parent: parent_class).call do |klass|
          klass.register_as(name)
          klass.relation(name)
          klass.inherit_header(inherit_header)

          klass.class_eval(&) if block_given?
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/header'
require 'rom/mapper/model_dsl'

module ROM
  class Mapper
    # Mapper attribute DSL exposed by mapper subclasses
    #
    # This class is private even though its methods are exposed by mappers.
    # Typically it's not meant to be used directly.
    #
    # TODO: break this madness down into smaller pieces
    #
    # @api private
    # rubocop:disable Metrics/ClassLength
    class AttributeDSL
      include ModelDSL

      attr_reader :attributes

      attr_reader :options

      attr_reader :copy_keys

      attr_reader :symbolize_keys

      attr_reader :reject_keys

      attr_reader :steps

      # @param [Array] attributes accumulator array
      # @param [Hash] options
      #
      # @api private
      def initialize(attributes, options)
        @attributes = attributes
        @options = options
        @copy_keys = options.fetch(:copy_keys)
        @symbolize_keys = options.fetch(:symbolize_keys)
        @prefix = options.fetch(:prefix)
        @prefix_separator = options.fetch(:prefix_separator)
        @reject_keys = options.fetch(:reject_keys)
        @steps = []
      end

      # Redefine the prefix for the following attributes
      #
      # @example
      #
      #   dsl = AttributeDSL.new([])
      #   dsl.attribute(:prefix, 'user')
      #
      # @api public
      def prefix(value = Undefined)
        if value.equal?(Undefined)
          @prefix
        else
          @prefix = value
        end
      end

      # Redefine the prefix separator for the following attributes
      #
      # @example
      #
      #   dsl = AttributeDSL.new([])
      #   dsl.attribute(:prefix_separator, '.')
      #
      # @api public
      def prefix_separator(value = Undefined)
        if value.equal?(Undefined)
          @prefix_separator
        else
          @prefix_separator = value
        end
      end

      # Define a mapping attribute with its options and/or block
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.attribute(:name)
      #   dsl.attribute(:email, from: 'user_email')
      #   dsl.attribute(:name) { 'John' }
      #   dsl.attribute(:name) { |t| t.upcase }
      #
      # @api public
      def attribute(name, options = EMPTY_HASH, &block)
        with_attr_options(name, options) do |attr_options|
          if options[:type] && block_given?
            raise ArgumentError,
                  "can't specify type and block at the same time"
          end
          attr_options[:coercer] = block if block_given?
          add_attribute(name, attr_options)
        end
      end

      def exclude(name)
        attributes << [name,  exclude: true]
      end

      # Perform transformations sequentially
      #
      # @example
      #   dsl = AttributeDSL.new()
      #
      #   dsl.step do
      #     attribute :name
      #   end
      #
      # @api public
      def step(options = EMPTY_HASH, &)
        steps << new(options, &)
      end

      # Define an embedded attribute
      #
      # Block exposes the attribute dsl too
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.embedded :tags, type: :array do
      #     attribute :name
      #   end
      #
      #   dsl.embedded :address, type: :hash do
      #     model Address
      #     attribute :name
      #   end
      #
      # @param [Symbol] name attribute
      #
      # @param [Hash] options
      # @option options [Symbol] :type Embedded type can be :hash or :array
      # @option options [Symbol] :prefix Prefix that should be used for
      #                                  its attributes
      #
      # @api public
      def embedded(name, options, &)
        with_attr_options(name) do |attr_options|
          mapper = options[:mapper]

          if mapper
            embedded_options = { type: :array }.update(options)
            attributes_from_mapper(
              mapper, name, embedded_options.update(attr_options)
            )
          else
            dsl = new(options, &)
            attr_options.update(options)
            add_attribute(
              name, { header: dsl.header, type: :array }.update(attr_options)
            )
          end
        end
      end

      # Define an embedded hash attribute that requires "wrapping" transformation
      #
      # Typically this is used in sql context when relation is a join.
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.wrap(address: [:street, :zipcode, :city])
      #
      #   dsl.wrap(:address) do
      #     model Address
      #     attribute :street
      #     attribute :zipcode
      #     attribute :city
      #   end
      #
      # @see AttributeDSL#embedded
      #
      # @api public
      def wrap(*args, &)
        ensure_mapper_configuration('wrap', args, block_given?)

        with_name_or_options(*args) do |name, options, mapper|
          wrap_options = { type: :hash, wrap: true }.update(options)

          if mapper
            attributes_from_mapper(mapper, name, wrap_options)
          else
            dsl(name, wrap_options, &)
          end
        end
      end

      # Define an embedded hash attribute that requires "unwrapping" transformation
      #
      # Typically this is used in no-sql context to normalize data before
      # inserting to sql gateway.
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.unwrap(address: [:street, :zipcode, :city])
      #
      #   dsl.unwrap(:address) do
      #     attribute :street
      #     attribute :zipcode
      #     attribute :city
      #   end
      #
      # @see AttributeDSL#embedded
      #
      # @api public
      def unwrap(*args, &)
        with_name_or_options(*args) do |name, options, mapper|
          unwrap_options = { type: :hash, unwrap: true }.update(options)

          if mapper
            attributes_from_mapper(mapper, name, unwrap_options)
          else
            dsl(name, unwrap_options, &)
          end
        end
      end

      # Define an embedded hash attribute that requires "grouping" transformation
      #
      # Typically this is used in sql context when relation is a join.
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.group(tags: [:name])
      #
      #   dsl.group(:tags) do
      #     model Tag
      #     attribute :name
      #   end
      #
      # @see AttributeDSL#embedded
      #
      # @api public
      def group(*args, &)
        ensure_mapper_configuration('group', args, block_given?)

        with_name_or_options(*args) do |name, options, mapper|
          group_options = { type: :array, group: true }.update(options)

          if mapper
            attributes_from_mapper(mapper, name, group_options)
          else
            dsl(name, group_options, &)
          end
        end
      end

      # Define an embedded array attribute that requires "ungrouping" transformation
      #
      # Typically this is used in non-sql context being prepared for import to sql.
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #   dsl.ungroup(tags: [:name])
      #
      # @see AttributeDSL#embedded
      #
      # @api public
      def ungroup(*args, &)
        with_name_or_options(*args) do |name, options, *|
          ungroup_options = { type: :array, ungroup: true }.update(options)
          dsl(name, ungroup_options, &)
        end
      end

      # Define an embedded hash attribute that requires "fold" transformation
      #
      # Typically this is used in sql context to fold single joined field
      # to the array of values.
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.fold(tags: [:name])
      #
      # @see AttributeDSL#embedded
      #
      # @api public
      def fold(*args, &)
        with_name_or_options(*args) do |name, *|
          fold_options = { type: :array, fold: true }
          dsl(name, fold_options, &)
        end
      end

      # Define an embedded hash attribute that requires "unfold" transformation
      #
      # Typically this is used in non-sql context to convert array of
      # values (like in Cassandra 'SET' or 'LIST' types) to array of tuples.
      #
      # Source values are assigned to the first key, the other keys being left blank.
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.unfold(tags: [:name, :type], from: :tags_list)
      #
      #   dsl.unfold :tags, from: :tags_list do
      #     attribute :name, from: :tag_name
      #     attribute :type, from: :tag_type
      #   end
      #
      # @see AttributeDSL#embedded
      #
      # @api public
      def unfold(name, options = EMPTY_HASH)
        with_attr_options(name, options) do |attr_options|
          old_name = attr_options.fetch(:from, name)
          dsl(old_name, type: :array, unfold: true) do
            attribute name, attr_options
            yield if block_given?
          end
        end
      end

      # Define an embedded combined attribute that requires "combine" transformation
      #
      # Typically this can be used to process results of eager-loading
      #
      # @example
      #   dsl = AttributeDSL.new([])
      #
      #   dsl.combine(:tags, user_id: :id) do
      #     model Tag
      #
      #     attribute :name
      #   end
      #
      # @param [Symbol] name
      # @param [Hash] options
      #   @option options [Hash] :on The "join keys"
      #   @option options [Symbol] :type The type, either :array (default) or :hash
      #
      # @api public
      def combine(name, options, &)
        dsl = new(options, &)

        attr_opts = {
          type: options.fetch(:type, :array),
          keys: options.fetch(:on),
          combine: true,
          header: dsl.header
        }

        add_attribute(name, attr_opts)
      end

      # Generate a header from attribute definitions
      #
      # @return [Header]
      #
      # @api private
      def header
        Header.coerce(attributes, copy_keys: copy_keys, model: model, reject_keys: reject_keys)
      end

      private

      # Remove the attribute used somewhere else (in wrap, group, model etc.)
      #
      # @api private
      def remove(*names)
        attributes.delete_if { |attr| names.include?(attr.first) }
      end

      # Handle attribute options common for all definitions
      #
      # @api private
      def with_attr_options(name, options = EMPTY_HASH)
        attr_options = options.dup

        if @prefix
          attr_options[:from] ||= "#{@prefix}#{@prefix_separator}#{name}"
          attr_options[:from] = attr_options[:from].to_sym if name.is_a? Symbol
        end

        attr_options.update(from: attr_options.fetch(:from) { name }.to_s) if symbolize_keys

        yield(attr_options)
      end

      # Handle "name or options" syntax used by `wrap` and `group`
      #
      # @api private
      def with_name_or_options(*args)
        name, options =
          if args.size > 1
            args
          else
            [args.first, {}]
          end

        yield(name, options, options[:mapper])
      end

      # Create another instance of the dsl for nested definitions
      #
      # This is used by embedded, wrap and group
      #
      # @api private
      def dsl(name_or_attrs, options, &)
        if block_given?
          attributes_from_block(name_or_attrs, options, &)
        else
          attributes_from_hash(name_or_attrs, options)
        end
      end

      # Define attributes from a nested block
      #
      # Used by embedded, wrap and group
      #
      # @api private
      def attributes_from_block(name, options, &)
        dsl = new(options, &)
        header = dsl.header
        add_attribute(name, options.update(header: header))
        header.each { |attr| remove(attr.key) unless name == attr.key }
      end

      # Define attributes from the `name => attributes` hash syntax
      #
      # Used by wrap and group
      #
      # @api private
      def attributes_from_hash(hash, options)
        hash.each do |name, header|
          with_attr_options(name, options) do |attr_options|
            add_attribute(name, attr_options.update(header: header.zip))
            header.each { |attr| remove(attr) unless name == attr }
          end
        end
      end

      # Infer mapper header for an embedded attribute
      #
      # @api private
      def attributes_from_mapper(mapper, name, options)
        if mapper.is_a?(Class)
          add_attribute(name, { header: mapper.header }.update(options))
        else
          raise(
            ArgumentError, ":mapper must be a class #{mapper.inspect}"
          )
        end
      end

      # Add a new attribute and make sure it overrides previous definition
      #
      # @api private
      def add_attribute(name, options)
        remove(name, name.to_s)
        attributes << [name, options]
      end

      # Create a new dsl instance of potentially overidden options
      #
      # Embedded, wrap and group can override top-level options like `prefix`
      #
      # @api private
      def new(options, &)
        dsl = self.class.new([], @options.merge(options))
        dsl.instance_exec(&) if block_given?
        dsl
      end

      # Ensure the mapping configuration isn't ambiguous
      #
      # @api private
      def ensure_mapper_configuration(method_name, args, block_present)
        if args.first.is_a?(Hash) && block_present
          raise MapperMisconfiguredError,
                "Cannot configure `#{method_name}#` using both options and a block"
        end
        if args.first.is_a?(Hash) && args.first[:mapper]
          raise MapperMisconfiguredError,
                "Cannot configure `#{method_name}#` using both options and a mapper"
        end
      end
    end
    # rubocop:enable Metrics/ClassLength
  end
end


def attribute(name, options = EMPTY_HASH, &block)
        with_attr_options(name, options) do |attr_options|
          if options[:type] && block_given?
            raise ArgumentError,
                  "can't specify type and block at the same time"
          end
          attr_options[:coercer] = block if block_given?
          add_attribute(name, attr_options)
        end
      end

attribute

      def with_attr_options(name, options = EMPTY_HASH)
        attr_options = options.dup

        if @prefix
          attr_options[:from] ||= "#{@prefix}#{@prefix_separator}#{name}"
          attr_options[:from] = attr_options[:from].to_sym if name.is_a? Symbol
        end

        attr_options.update(from: attr_options.fetch(:from) { name }.to_s) if symbolize_keys

        yield(attr_options)
      end

      def add_attribute(name, options)
        remove(name, name.to_s)
        attributes << [name, options]
      end


def embedded(name, options, &)
        with_attr_options(name) do |attr_options|
          mapper = options[:mapper]

          if mapper
            embedded_options = { type: :array }.update(options)
            attributes_from_mapper(
              mapper, name, embedded_options.update(attr_options)
            )
          else
            dsl = new(options, &)
            attr_options.update(options)
            add_attribute(
              name, { header: dsl.header, type: :array }.update(attr_options)
            )
          end
        end
      end

embedded

      def attributes_from_mapper(mapper, name, options)
        if mapper.is_a?(Class)
          add_attribute(name, { header: mapper.header }.update(options))
        else
          raise(
            ArgumentError, ":mapper must be a class #{mapper.inspect}"
          )
        end
      end


def wrap(*args, &)
        ensure_mapper_configuration('wrap', args, block_given?)

        with_name_or_options(*args) do |name, options, mapper|
          wrap_options = { type: :hash, wrap: true }.update(options)

          if mapper
            attributes_from_mapper(mapper, name, wrap_options)
          else
            dsl(name, wrap_options, &)
          end
        end
      end

wrap

      def ensure_mapper_configuration(method_name, args, block_present)
        if args.first.is_a?(Hash) && block_present
          raise MapperMisconfiguredError,
                "Cannot configure `#{method_name}#` using both options and a block"
        end
        if args.first.is_a?(Hash) && args.first[:mapper]
          raise MapperMisconfiguredError,
                "Cannot configure `#{method_name}#` using both options and a mapper"
        end
      end

      def with_name_or_options(*args)
        name, options =
          if args.size > 1
            args
          else
            [args.first, {}]
          end

        yield(name, options, options[:mapper])
      end

dsl

      def dsl(name_or_attrs, options, &)
        if block_given?
          attributes_from_block(name_or_attrs, options, &)
        else
          attributes_from_hash(name_or_attrs, options)
        end
      end


def unwrap(*args, &)
        with_name_or_options(*args) do |name, options, mapper|
          unwrap_options = { type: :hash, unwrap: true }.update(options)

          if mapper
            attributes_from_mapper(mapper, name, unwrap_options)
          else
            dsl(name, unwrap_options, &)
          end
        end
      end


def group(*args, &)
        ensure_mapper_configuration('group', args, block_given?)

        with_name_or_options(*args) do |name, options, mapper|
          group_options = { type: :array, group: true }.update(options)

          if mapper
            attributes_from_mapper(mapper, name, group_options)
          else
            dsl(name, group_options, &)
          end
        end
      end


def combine(name, options, &)
        dsl = new(options, &)

        attr_opts = {
          type: options.fetch(:type, :array),
          keys: options.fetch(:on),
          combine: true,
          header: dsl.header
        }

        add_attribute(name, attr_opts)
      end


def ensure_mapper_configuration(method_name, args, block_present)
        if args.first.is_a?(Hash) && block_present
          raise MapperMisconfiguredError,
                "Cannot configure `#{method_name}#` using both options and a block"
        end
        if args.first.is_a?(Hash) && args.first[:mapper]
          raise MapperMisconfiguredError,
                "Cannot configure `#{method_name}#` using both options and a mapper"
        end
      end


# frozen_string_literal: true

require 'transproc/all'

require 'rom/processor'

module ROM
  class Processor
    # Data mapping transformer builder using Transproc
    #
    # This builds a transproc function that is used to map a whole relation
    #
    # @see https://github.com/solnic/transproc too
    #
    # @private
    # rubocop:disable Metrics/ClassLength
    class Transproc < Processor
      include ::Transproc::Composer

      module Functions
        extend ::Transproc::Registry

        import ::Transproc::Coercions
        import ::Transproc::ArrayTransformations
        import ::Transproc::HashTransformations
        import ::Transproc::ClassTransformations
        import ::Transproc::ProcTransformations
        import ::Transproc::Conditional

        INVALID_INJECT_UNION_VALUE = '%s attribute: block is required for :from with union value.'

        def self.get(arr, idx)
          arr[idx]
        end

        def self.filter_empty(arr)
          arr.reject { |row| row.values.all?(&:nil?) }
        end

        def self.inject_union_value(tuple, name, keys, coercer)
          raise ROM::MapperMisconfiguredError, INVALID_INJECT_UNION_VALUE % [name] unless coercer

          values = tuple.values_at(*keys)
          result = coercer.call(*values)

          tuple.merge(name => result)
        end
      end

      # @return [Mapper] mapper that this processor belongs to
      #
      # @api private
      attr_reader :mapper

      # @return [Header] header from a mapper
      #
      # @api private
      attr_reader :header

      # @return [Class] model class from a mapper
      #
      # @api private
      attr_reader :model

      # @return [Hash] header's attribute mapping
      #
      # @api private
      attr_reader :mapping

      # @return [Proc] row-processing proc
      #
      # @api private
      attr_reader :row_proc

      # Build a transproc function from the header
      #
      # @param [ROM::Header] header
      #
      # @return [Transproc::Function]
      #
      # @api private
      def self.build(mapper, header)
        new(mapper, header).to_transproc
      end

      # @api private
      def initialize(mapper, header)
        super()
        @mapper = mapper
        @header = header
        @model = header.model
        @mapping = header.mapping
        initialize_row_proc
      end

      # Coerce mapper header to a transproc data mapping function
      #
      # @return [Transproc::Function]
      #
      # @api private
      # rubocop:disable Metrics/AbcSize
      def to_transproc
        compose(t(:identity)) do |ops|
          combined = header.combined
          ops << t(:combine, combined.map(&method(:combined_args))) if combined.any?
          ops << header.preprocessed.map { |attr| visit(attr, true) }
          ops << t(:map_array, row_proc) if row_proc
          ops << header.postprocessed.map { |attr| visit(attr, true) }
        end
      end
      # rubocop:enable Metrics/AbcSize

      private

      # Visit an attribute from the header
      #
      # This forwards to a specialized visitor based on the attribute type
      #
      # @param [Header::Attribute] attribute
      # @param [Array] args Allows to send `preprocess: true`
      #
      # @api private
      def visit(attribute, *args)
        type = attribute.class.name.split('::').last.downcase
        send("visit_#{type}", attribute, *args)
      end

      # Visit plain attribute
      #
      # It will call block transformation if it's used
      #
      # If it's a typed attribute a coercion transformation is added
      #
      # @param [Header::Attribute] attribute
      #
      # @api private
      #
      # rubocop:disable Metrics/AbcSize
      def visit_attribute(attribute)
        coercer = attribute.meta[:coercer]
        if attribute.union?
          compose do |ops|
            ops << t(:inject_union_value, attribute.name, attribute.key, coercer)
            ops << t(:reject_keys, attribute.key) unless header.copy_keys
          end
        elsif coercer
          t(:map_value, attribute.name, t(:bind, mapper, coercer))
        elsif attribute.typed?
          t(:map_value, attribute.name, t(:"to_#{attribute.type}"))
        end
      end
      # rubocop:enable Metrics/AbcSize

      # Visit hash attribute
      #
      # @param [Header::Attribute::Hash] attribute
      #
      # @api private
      def visit_hash(attribute)
        with_row_proc(attribute) do |row_proc|
          t(:map_value, attribute.name, row_proc)
        end
      end

      # Visit combined attribute
      #
      # @api private
      def visit_combined(attribute)
        op = with_row_proc(attribute) do |row_proc|
          array_proc =
            if attribute.type == :hash
              t(:map_array, row_proc) >> t(:get, 0)
            else
              t(:map_array, row_proc)
            end

          t(:map_value, attribute.name, array_proc)
        end

        if op
          op
        elsif attribute.type == :hash
          t(:map_value, attribute.name, t(:get, 0))
        end
      end

      # Visit array attribute
      #
      # @param [Header::Attribute::Array] attribute
      #
      # @api private
      def visit_array(attribute)
        with_row_proc(attribute) do |row_proc|
          t(:map_value, attribute.name, t(:map_array, row_proc))
        end
      end

      # Visit wrapped hash attribute
      #
      # :nest transformation is added to handle wrapping
      #
      # @param [Header::Attribute::Wrap] attribute
      #
      # @api private
      def visit_wrap(attribute)
        name = attribute.name
        keys = attribute.tuple_keys

        compose do |ops|
          ops << t(:nest, name, keys)
          ops << visit_hash(attribute)
        end
      end

      # Visit unwrap attribute
      #
      # :unwrap transformation is added to handle unwrapping
      #
      # @param [Header::Attributes::Unwrap] attribute
      #
      # @api private
      def visit_unwrap(attribute)
        name = attribute.name
        keys = attribute.pop_keys

        compose do |ops|
          ops << visit_hash(attribute)
          ops << t(:unwrap, name, keys)
        end
      end

      # Visit group hash attribute
      #
      # :group transformation is added to handle grouping during preprocessing.
      # Otherwise we simply use array visitor for the attribute.
      #
      # @param [Header::Attribute::Group] attribute
      # @param [Boolean] preprocess true if we are building a relation preprocessing
      #                             function that is applied to the whole relation
      #
      # @api private
      def visit_group(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        if preprocess
          name = attribute.name
          header = attribute.header
          keys = attribute.tuple_keys

          others = header.preprocessed

          compose do |ops|
            ops << t(:group, name, keys)
            ops << t(:map_array, t(:map_value, name, t(:filter_empty)))
            ops << others.map { |attr|
              t(:map_array, t(:map_value, name, visit(attr, true)))
            }
          end
        else
          visit_array(attribute)
        end
      end

      # Visit ungroup attribute
      #
      # :ungroup transforation is added to handle ungrouping during preprocessing.
      # Otherwise we simply use array visitor for the attribute.
      #
      # @param [Header::Attribute::Ungroup] attribute
      # @param [Boolean] preprocess true if we are building a relation preprocessing
      #                             function that is applied to the whole relation
      #
      # @api private
      def visit_ungroup(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        if preprocess
          name = attribute.name
          header = attribute.header
          keys = attribute.pop_keys

          others = header.postprocessed

          compose do |ops|
            ops << others.map { |attr|
              t(:map_array, t(:map_value, name, visit(attr, true)))
            }
            ops << t(:ungroup, name, keys)
          end
        else
          visit_array(attribute)
        end
      end

      # Visit fold hash attribute
      #
      # :fold transformation is added to handle folding during preprocessing.
      #
      # @param [Header::Attribute::Fold] attribute
      # @param [Boolean] preprocess true if we are building a relation preprocessing
      #                             function that is applied to the whole relation
      #
      # @api private
      def visit_fold(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        if preprocess
          name = attribute.name
          keys = attribute.tuple_keys

          compose do |ops|
            ops << t(:group, name, keys)
            ops << t(:map_array, t(:map_value, name, t(:filter_empty)))
            ops << t(:map_array, t(:fold, name, keys.first))
          end
        end
      end

      # Visit unfold hash attribute
      #
      # :unfold transformation is added to handle unfolding during preprocessing.
      #
      # @param [Header::Attribute::Unfold] attribute
      # @param [Boolean] preprocess true if we are building a relation preprocessing
      #                             function that is applied to the whole relation
      #
      # @api private
      # rubocop:disable Metrics/AbcSize
      def visit_unfold(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        return unless preprocess

        name = attribute.name
        header = attribute.header
        keys = attribute.pop_keys
        key = keys.first

        others = header.postprocessed

        compose do |ops|
          ops << others.map { |attr|
            t(:map_array, t(:map_value, name, visit(attr, true)))
          }
          ops << t(:map_array, t(:map_value, name, t(:insert_key, key)))
          ops << t(:map_array, t(:reject_keys, [key] - [name]))
          ops << t(:ungroup, name, [key])
        end
      end
      # rubocop:enable Metrics/AbcSize

      # Visit excluded attribute
      #
      # @param [Header::Attribute::Exclude] attribute
      #
      # @api private
      def visit_exclude(attribute)
        t(:reject_keys, [attribute.name])
      end

      # @api private
      def combined_args(attribute)
        other = attribute.header.combined

        if other.any?
          children = other.map(&method(:combined_args))
          [attribute.name, attribute.meta[:keys], children]
        else
          [attribute.name, attribute.meta[:keys]]
        end
      end

      # Build row_proc
      #
      # This transproc function is applied to each row in a dataset
      #
      # @api private
      def initialize_row_proc
        @row_proc = compose { |ops|
          alias_handler = header.copy_keys ? :copy_keys : :rename_keys
          process_header_keys(ops)

          ops << t(alias_handler, mapping) if header.aliased?
          ops << header.map { |attr| visit(attr) }
          ops << t(:constructor_inject, model) if model
        }
      end

      # Process row_proc header keys
      #
      # @api private
      def process_header_keys(ops)
        if header.reject_keys
          all_keys = header.tuple_keys + header.non_primitives.map(&:key)
          ops << t(:accept_keys, all_keys)
        end
        ops
      end

      # Yield row proc for a given attribute if any
      #
      # @param [Header::Attribute] attribute
      #
      # @api private
      def with_row_proc(attribute)
        row_proc = row_proc_from(attribute)
        yield(row_proc) if row_proc
      end

      # Build a row_proc from a given attribute
      #
      # This is used by embedded attribute visitors
      #
      # @api private
      def row_proc_from(attribute)
        new(mapper, attribute.header).row_proc
      end

      # Return a new instance of the processor
      #
      # @api private
      def new(*args)
        self.class.new(*args)
      end

      # @api private
      def t(*args)
        Functions[*args]
      end
    end
    # rubocop:enable Metrics/ClassLength
  end
end

t

      def t(*args)
        Functions[*args]
      end


def visit_combined(attribute)
        op = with_row_proc(attribute) do |row_proc|
          array_proc =
            if attribute.type == :hash
              t(:map_array, row_proc) >> t(:get, 0)
            else
              t(:map_array, row_proc)
            end

          t(:map_value, attribute.name, array_proc)
        end

        if op
          op
        elsif attribute.type == :hash
          t(:map_value, attribute.name, t(:get, 0))
        end
      end

      def with_row_proc(attribute)
        row_proc = row_proc_from(attribute)
        yield(row_proc) if row_proc
      end


def visit_group(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        if preprocess
          name = attribute.name
          header = attribute.header
          keys = attribute.tuple_keys

          others = header.preprocessed

          compose do |ops|
            ops << t(:group, name, keys)
            ops << t(:map_array, t(:map_value, name, t(:filter_empty)))
            ops << others.map { |attr|
              t(:map_array, t(:map_value, name, visit(attr, true)))
            }
          end
        else
          visit_array(attribute)
        end
      end

visit

      def visit(attribute, *args)
        type = attribute.class.name.split('::').last.downcase
        send("visit_#{type}", attribute, *args)
      end

      def visit_array(attribute)
        with_row_proc(attribute) do |row_proc|
          t(:map_value, attribute.name, t(:map_array, row_proc))
        end
      end


def visit_ungroup(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        if preprocess
          name = attribute.name
          header = attribute.header
          keys = attribute.pop_keys

          others = header.postprocessed

          compose do |ops|
            ops << others.map { |attr|
              t(:map_array, t(:map_value, name, visit(attr, true)))
            }
            ops << t(:ungroup, name, keys)
          end
        else
          visit_array(attribute)
        end
      end


def visit_fold(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        if preprocess
          name = attribute.name
          keys = attribute.tuple_keys

          compose do |ops|
            ops << t(:group, name, keys)
            ops << t(:map_array, t(:map_value, name, t(:filter_empty)))
            ops << t(:map_array, t(:fold, name, keys.first))
          end
        end
      end


def visit_unfold(attribute, preprocess = false) # rubocop:disable Style/OptionalBooleanParameter
        return unless preprocess

        name = attribute.name
        header = attribute.header
        keys = attribute.pop_keys
        key = keys.first

        others = header.postprocessed

        compose do |ops|
          ops << others.map { |attr|
            t(:map_array, t(:map_value, name, visit(attr, true)))
          }
          ops << t(:map_array, t(:map_value, name, t(:insert_key, key)))
          ops << t(:map_array, t(:reject_keys, [key] - [name]))
          ops << t(:ungroup, name, [key])
        end
      end