# frozen_string_literal: true

require 'rom/commands/composite'
require 'rom/commands/graph'

module ROM
  module Commands
    # Lazy command wraps another command and evaluates its input when called
    #
    # @api private
    class Lazy
      include Dry::Equalizer(:command, :evaluator)

      # @attr_reader [Command] command The wrapped command
      attr_reader :command

      alias_method :unwrap, :command

      # @attr_reader [Proc] evaluator The proc that will evaluate the input
      attr_reader :evaluator

      attr_reader :command_proc

      # @api private
      def self.[](command)
        case command
        when Commands::Create then Lazy::Create
        when Commands::Update then Lazy::Update
        when Commands::Delete then Lazy::Delete
        else
          self
        end
      end

      # @api private
      def initialize(command, evaluator, command_proc = nil)
        @command = command
        @evaluator = evaluator
        @command_proc = command_proc || proc { |*| command }
      end

      # Evaluate command's input using the input proc and pass to command
      #
      # @return [Array,Hash]
      #
      # @api public
      def call(*_args)
        raise NotImplementedError
      end

      # Compose a lazy command with another one
      #
      # @see Commands::Abstract#>>
      #
      # @return [Composite]
      #
      # @api public
      def >>(other)
        Composite.new(self, other)
      end

      # Combine with other lazy commands
      #
      # @see Abstract#combine
      #
      # @return [Graph]
      #
      # @api public
      def combine(*others)
        Graph.new(self, others)
      end

      # @api private
      def lazy?
        true
      end

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

      private

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

          if response.instance_of?(command.class)
            self.class.new(response, evaluator, command_proc)
          else
            response
          end
        else
          super
        end
      end
    end
  end
end

require 'rom/commands/lazy/create'
require 'rom/commands/lazy/update'
require 'rom/commands/lazy/delete'


# frozen_string_literal: true

require 'rom/pipeline'

module ROM
  module Commands
    # Composite command that consists of left and right commands
    #
    # @api public
    class Composite < Pipeline::Composite
      # Calls the composite command
      #
      # Right command is called with a result from the left one
      #
      # @return [Object]
      #
      # @api public
      #
      # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
      def call(*args)
        response = left.call(*args)

        if response.nil? || (many? && response.empty?)
          return one? ? nil : EMPTY_ARRAY
        end

        if one? && !graph?
          if right.is_a?(Command) || right.is_a?(Commands::Composite)
            right.call([response].first)
          else
            right.call([response]).first
          end
        elsif one? && graph?
          right.call(response).first
        else
          right.call(response)
        end
      end
      alias_method :[], :call
      # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

      # @api private
      def graph?
        left.is_a?(Graph)
      end

      # @api private
      def result
        left.result
      end

      # @api private
      def decorate?(response)
        super || response.is_a?(Graph)
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/support/inflector'

module ROM
  # Base command class with factory class-level interface and setup-related logic
  #
  # @private
  class Command
    module ClassInterface
      # This hook sets up default class state
      #
      # @api private
      def inherited(klass)
        super
        klass.instance_variable_set(:@before, before.dup)
        klass.instance_variable_set(:@after, after.dup)
      end

      # Sets up the base class
      #
      # @api private
      def self.extended(klass)
        super
        klass.set_hooks(:before, [])
        klass.set_hooks(:after, [])
      end

      # Return adapter specific sub-class based on the adapter identifier
      #
      # This is a syntax sugar to make things consistent
      #
      # @example
      #   ROM::Commands::Create[:memory]
      #   # => ROM::Memory::Commands::Create
      #
      # @param [Symbol] adapter identifier
      #
      # @return [Class]
      #
      # @api public
      def [](adapter)
        adapter_namespace(adapter).const_get(Inflector.demodulize(name))
      end

      # Return namespaces that contains command subclasses of a specific adapter
      #
      # @param [Symbol] adapter identifier
      #
      # @return [Module]
      #
      # @api private
      def adapter_namespace(adapter)
        ROM.adapters.fetch(adapter).const_get(:Commands)
      rescue KeyError
        raise AdapterNotPresentError.new(adapter, :relation)
      end

      # Build a command class for a specific relation with options
      #
      # @example
      #   class CreateUser < ROM::Commands::Create[:memory]
      #   end
      #
      #   command = CreateUser.build(rom.relations[:users])
      #
      # @param [Relation] relation
      # @param [Hash] options
      #
      # @return [Command]
      #
      # @api public
      def build(relation, **options)
        new(relation, **self.options, **options)
      end

      # Create a command class with a specific type
      #
      # @param [Symbol] name Command name
      # @param [Class] type Command class
      #
      # @yield [Class]
      #
      # @return [Class, Object]
      #
      # @api public
      def create_class(name, type, &)
        klass = Dry::Core::ClassBuilder
          .new(name: "#{Inflector.classify(type)}[:#{name}]", parent: type)
          .call

        if block_given?
          yield(klass)
        else
          klass
        end
      end

      # Use a configured plugin in this relation
      #
      # @example
      #   class CreateUser < ROM::Commands::Create[:memory]
      #     use :pagintion
      #
      #     per_page 30
      #   end
      #
      # @param [Symbol] plugin
      # @param [Hash] options
      # @option options [Symbol] :adapter (:default) first adapter to check for plugin
      #
      # @api public
      def use(plugin, **options)
        ROM.plugin_registry[:command].fetch(plugin, adapter).apply_to(self, **options)
      end

      # Extend a command class with relation view methods
      #
      # @param [Relation] relation
      #
      # @return [Class]
      #
      # @api public
      def extend_for_relation(relation)
        include(relation_methods_mod(relation.class))
      end

      # Set before-execute hooks
      #
      # @overload before(hook)
      #   Set an before hook as a method name
      #
      #   @example
      #     class CreateUser < ROM::Commands::Create[:sql]
      #       relation :users
      #       register_as :create
      #
      #       before :my_hook
      #
      #       def my_hook(tuple, *)
      #         puts "hook called#
      #       end
      #     end
      #
      # @overload before(hook_opts)
      #   Set an before hook as a method name with arguments
      #
      #   @example
      #     class CreateUser < ROM::Commands::Create[:sql]
      #       relation :users
      #       register_as :create
      #
      #       before my_hook: { arg1: 1, arg2: 2 }
      #
      #       def my_hook(tuple, arg1:, arg2:)
      #         puts "hook called with args: #{arg1} and #{arg2}"
      #       end
      #     end
      #
      #   @param [Hash<Symbol=>Hash>] hook Options with method name and pre-set args
      #
      # @return [Array<Hash, Symbol>] A list of all configured before hooks
      #
      # @api public
      def before(*hooks)
        if hooks.empty?
          @before
        else
          set_hooks(:before, hooks)
        end
      end

      # Set after-execute hooks
      #
      # @overload after(hook)
      #   Set an after hook as a method name
      #
      #   @example
      #     class CreateUser < ROM::Commands::Create[:sql]
      #       relation :users
      #       register_as :create
      #
      #       after :my_hook
      #
      #       def my_hook(tuple, *)
      #         puts "hook called#
      #       end
      #     end
      #
      # @overload after(hook_opts)
      #   Set an after hook as a method name with arguments
      #
      #   @example
      #     class CreateUser < ROM::Commands::Create[:sql]
      #       relation :users
      #       register_as :create
      #
      #       after my_hook: { arg1: 1, arg1: 2 }
      #
      #       def my_hook(tuple, arg1:, arg2:)
      #         puts "hook called with args: #{arg1} and #{arg2}"
      #       end
      #     end
      #
      #   @param [Hash<Symbol=>Hash>] hook Options with method name and pre-set args
      #
      # @return [Array<Hash, Symbol>] A list of all configured after hooks
      #
      # @api public
      def after(*hooks)
        if hooks.empty?
          @after
        else
          set_hooks(:after, hooks)
        end
      end

      # Set new or more hooks
      #
      # @api private
      def set_hooks(type, hooks)
        ivar = :"@#{type}"

        if instance_variable_defined?(ivar)
          instance_variable_get(ivar).concat(hooks)
        else
          instance_variable_set(ivar, hooks)
        end
      end

      # Return default name of the command class based on its name
      #
      # During setup phase this is used by defalut as `register_as` option
      #
      # @return [Symbol]
      #
      # @api private
      def default_name
        Inflector.underscore(Inflector.demodulize(name)).to_sym
      end

      # Return default options based on class macros
      #
      # @return [Hash]
      #
      # @api private
      def options
        { input: input, result: result, before: before, after: after }
      end

      # @api private
      def relation_methods_mod(relation_class)
        Module.new do
          relation_class.view_methods.each do |meth|
            module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
              def #{meth}(*args, **kwargs)                                 # def create(*args, **kwargs)
                response = relation.public_send(:#{meth}, *args, **kwargs) #   response = relation.public_send(:create, *args, **kwargs)
                                                                           #
                if response.is_a?(relation.class)                          #   if response.is_a?(relation.class)
                  new(response)                                            #     new(response)
                else                                                       #   else
                  response                                                 #     response
                end                                                        #   end
              end                                                          # end
            RUBY
          end
        end
      end
    end
  end
end


def relation_methods_mod(relation_class)
        Module.new do
          relation_class.view_methods.each do |meth|
            module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
              def #{meth}(*args, **kwargs)                                 # def create(*args, **kwargs)
                response = relation.public_send(:#{meth}, *args, **kwargs) #   response = relation.public_send(:create, *args, **kwargs)
                                                                           #
                if response.is_a?(relation.class)                          #   if response.is_a?(relation.class)
                  new(response)                                            #     new(response)
                else                                                       #   else
                  response                                                 #     response
                end                                                        #   end
              end                                                          # end
            RUBY
          end
        end
      end


# frozen_string_literal: true

require 'rom/support/inflector'

require 'rom/associations/definitions'

module ROM
  class Schema
    # Additional schema DSL for definition SQL associations
    #
    # This DSL is exposed in `associations do .. end` blocks in schema defintions.
    #
    # @api public
    class AssociationsDSL < BasicObject
      # @!attribute [r] source
      #   @return [Relation::Name] The source relation
      attr_reader :source

      # @!attribute [r] registry
      #   @return [RelationRegistry] Relations registry from a rom container
      attr_reader :registry

      # @api private
      def initialize(source, &)
        @source = source
        @registry = {}
        instance_exec(&)
      end

      # Establish a one-to-many association
      #
      # @example using relation identifier
      #   has_many :tasks
      #
      # @example setting custom foreign key name
      #   has_many :tasks, foreign_key: :assignee_id
      #
      # @example with a :through option
      #   # this establishes many-to-many association
      #   has_many :tasks, through: :users_tasks
      #
      # @example using a custom view which overrides default one
      #   has_many :posts, view: :published, override: true
      #
      # @example using aliased association with a custom view
      #   has_many :posts, as: :published_posts, view: :published
      #
      # @example using custom target relation
      #   has_many :user_posts, relation: :posts
      #
      # @example using custom target relation and an alias
      #   has_many :user_posts, relation: :posts, as: :published, view: :published
      #
      # @param [Symbol] target The target relation identifier
      # @param [Hash] options A hash with additional options
      #
      # @return [Associations::OneToMany]
      #
      # @see #many_to_many
      #
      # @api public
      def one_to_many(target, **options)
        if options[:through]
          many_to_many(target, **options)
        else
          add(::ROM::Associations::Definitions::OneToMany.new(source, target, **options))
        end
      end
      alias_method :has_many, :one_to_many

      # Establish a one-to-one association
      #
      # @example using relation identifier
      #   one_to_one :addresses, as: :address
      #
      # @example with an intermediate join relation
      #   one_to_one :tasks, as: :priority_task, through: :assignments
      #
      # @param [Symbol] target The target relation identifier
      # @param [Hash] options A hash with additional options
      #
      # @return [Associations::OneToOne]
      #
      # @see #belongs_to
      #
      # @api public
      def one_to_one(target, **options)
        if options[:through]
          one_to_one_through(target, **options)
        else
          add(::ROM::Associations::Definitions::OneToOne.new(source, target, **options))
        end
      end

      # Establish a one-to-one association with a :through option
      #
      # @example
      #   one_to_one_through :users, as: :author, through: :users_posts
      #
      # @return [Associations::OneToOneThrough]
      #
      # @api public
      def one_to_one_through(target, **options)
        add(::ROM::Associations::Definitions::OneToOneThrough.new(source, target, **options))
      end

      # Establish a many-to-many association
      #
      # @example using relation identifier
      #   many_to_many :tasks, through: :users_tasks
      #
      # @param [Symbol] target The target relation identifier
      # @param [Hash] options A hash with additional options
      #
      # @return [Associations::ManyToMany]
      #
      # @see #one_to_many
      #
      # @api public
      def many_to_many(target, **options)
        add(::ROM::Associations::Definitions::ManyToMany.new(source, target, **options))
      end

      # Establish a many-to-one association
      #
      # @example using relation identifier
      #   many_to_one :users, as: :author
      #
      # @param [Symbol] target The target relation identifier
      # @param [Hash] options A hash with additional options
      #
      # @return [Associations::ManyToOne]
      #
      # @see #one_to_many
      #
      # @api public
      def many_to_one(target, **options)
        add(::ROM::Associations::Definitions::ManyToOne.new(source, target, **options))
      end

      # Shortcut for many_to_one which sets alias automatically
      #
      # @example with an alias (relation identifier is inferred via pluralization)
      #   belongs_to :user
      #
      # @example with an explicit alias
      #   belongs_to :users, as: :author
      #
      # @see #many_to_one
      #
      # @return [Associations::ManyToOne]
      #
      # @api public
      def belongs_to(target, **options)
        many_to_one(dataset_name(target), as: target, **options)
      end

      # Shortcut for one_to_one which sets alias automatically
      #
      # @example with an alias (relation identifier is inferred via pluralization)
      #   has_one :address
      #
      # @example with an explicit alias and a custom view
      #   has_one :posts, as: :priority_post, view: :prioritized
      #
      # @see #one_to_one
      #
      # @return [Associations::OneToOne]
      #
      # @api public
      def has_one(target, **options)
        one_to_one(dataset_name(target), as: target, **options)
      end

      # Return an association set for a schema
      #
      # @return [AssociationSet]
      #
      # @api private
      def call = AssociationSet[source.relation].build(registry)

      private

      # @api private
      def add(association)
        key = association.as || association.name

        if registry.key?(key)
          ::Kernel.raise(
            ::ArgumentError,
            "association #{key.inspect} is already defined for #{source.to_sym.inspect} relation"
          )
        end

        registry[key] = association
      end

      # @api private
      def dataset_name(name) = Inflector.pluralize(name).to_sym
    end
  end
end


# frozen_string_literal: true

require 'rom/types'
require 'rom/attribute'
require 'rom/schema/associations_dsl'

module ROM
  class Schema
    # Schema DSL exposed as `schema { .. }` in relation classes
    #
    # @api public
    class DSL < ::BasicObject
      KERNEL_METHODS = %i[extend method block_given?].freeze
      KERNEL_METHODS.each { |m| define_method(m, ::Kernel.instance_method(m)) }

      extend Initializer

      # @!attribute [r] relation
      #   @return [Relation::Name] The name of the schema's relation
      param :relation

      # @!attribute [r] inferrer
      #   @return [Inferrer] Optional attribute inferrer
      option :inferrer, default: -> { DEFAULT_INFERRER }

      # @!attribute [r] schema_class
      #   @return [Class] Schema class that should be instantiated
      option :schema_class, default: -> { Schema }

      # @!attribute [r] attr_class
      #   @return [Class] Attribute class that should be used
      option :attr_class, default: -> { Attribute }

      # @!attribute [r] adapter
      #   @return [Symbol] The adapter identifier used in gateways
      option :adapter, default: -> { :default }

      # @!attribute [r] attributes
      #   @return [Hash<Symbol, Hash>] A hash with attribute names as
      #   keys and attribute representations as values.
      #
      #   @see [Schema.build_attribute_info]
      attr_reader :attributes

      # @!attribute [r] plugins
      #   @return [Hash] A hash with schema plugins enabled in a schema
      attr_reader :plugins

      # @!attribute [r] definition
      #   @return [Proc] Definition block passed to DSL
      attr_reader :definition

      # @!attribute [r] associations_dsl
      #   @return [AssociationDSL] Associations defined within a block
      attr_reader :associations_dsl

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

        @attributes = {}
        @plugins = {}

        @definition = block
      end

      # Defines a relation attribute with its type and options.
      #
      # When only options are given, type is left as nil. It makes
      # sense when it is used alongside an schema inferrer, which will
      # populate the type.
      #
      # @see Relation.schema
      #
      # @api public
      def attribute(name, type_or_options, options = EMPTY_HASH)
        if attributes.key?(name)
          ::Kernel.raise ::ROM::AttributeAlreadyDefinedError,
                         "Attribute #{name.inspect} already defined"
        end

        attributes[name] = build_attribute_info(name, type_or_options, options)
      end

      # Define associations for a relation
      #
      # @example
      #   class Users < ROM::Relation[:sql]
      #     schema(infer: true) do
      #       associations do
      #         has_many :tasks
      #         has_many :posts
      #         has_many :posts, as: :priority_posts, view: :prioritized
      #         belongs_to :account
      #       end
      #     end
      #   end
      #
      #   class Posts < ROM::Relation[:sql]
      #     schema(infer: true) do
      #       associations do
      #         belongs_to :users, as: :author
      #       end
      #     end
      #
      #     view(:prioritized) do
      #       where { priority <= 3 }
      #     end
      #   end
      #
      # @return [AssociationDSL]
      #
      # @api public
      def associations(&)
        @associations_dsl = AssociationsDSL.new(relation, &)
      end

      # Builds a representation of the information needed to create an
      # attribute. It returns a hash with `:type` and `:options` keys.
      #
      # @return [Hash]
      #
      # @see [Schema.build_attribute_info]
      #
      # @api private
      def build_attribute_info(name, type_or_options, options = EMPTY_HASH)
        type, options = if type_or_options.is_a?(::Hash)
                          [nil, type_or_options]
                        else
                          [build_type(type_or_options, options), options]
                        end
        Schema.build_attribute_info(
          type, **options, name: name
        )
      end

      # Builds a type instance from base type and meta options
      #
      # @return [Dry::Types::Type] Type instance
      #
      # @api private
      # rubocop:disable Metrics/AbcSize
      def build_type(type, options = EMPTY_HASH)
        if options[:read]
          type.meta(source: relation, read: options[:read])
        elsif type.optional? && type.meta[:read]
          type.meta(source: relation, read: type.meta[:read].optional)
        else
          type.meta(source: relation)
        end.meta(Attribute::META_OPTIONS.map { |opt|
                   [opt, options[opt]] if options.key?(opt)
                 }.compact.to_h)
      end
      # rubocop:enable Metrics/AbcSize

      # Specify which key(s) should be the primary key
      #
      # @api public
      def primary_key(*names)
        names.each do |name|
          attributes[name][:type] =
            attributes[name][:type].meta(primary_key: true)
        end
        self
      end

      # Enables for the schema
      #
      # @param [Symbol] plugin_name Plugin name
      # @param [Hash] options Plugin options
      #
      # @api public
      def use(plugin_name, options = ::ROM::EMPTY_HASH)
        plugin = ::ROM.plugin_registry[:schema].fetch(plugin_name, adapter)
        app_plugin(plugin, options)
      end

      # @api private
      def app_plugin(plugin, options = ::ROM::EMPTY_HASH)
        plugin.extend_dsl(self)
        @plugins[plugin.name] = [plugin, plugin.config.to_hash.merge(options)]
      end

      # @api private
      def call(&)
        instance_exec(&) if block_given?
        instance_exec(&definition) if definition

        schema_class.define(relation, **opts) do |schema|
          plugins.each_value do |plugin, options|
            plugin.apply_to(schema, **options)
          end
        end
      end

      # @api private
      def plugin_options(plugin) = @plugins[plugin][1]

      private

      # Return schema opts
      #
      # @return [Hash]
      #
      # @api private
      def opts
        opts = { attributes: attributes.values, inferrer: inferrer, attr_class: attr_class }

        if associations_dsl
          { **opts, associations: associations_dsl.call }
        else
          opts
        end
      end
    end
  end
end


def build_type(type, options = EMPTY_HASH)
        if options[:read]
          type.meta(source: relation, read: options[:read])
        elsif type.optional? && type.meta[:read]
          type.meta(source: relation, read: type.meta[:read].optional)
        else
          type.meta(source: relation)
        end.meta(Attribute::META_OPTIONS.map { |opt|
                   [opt, options[opt]] if options.key?(opt)
                 }.compact.to_h)
      end


# frozen_string_literal: true

module ROM
  class Schema
    # @api private
    class Inferrer
      extend Dry::Core::ClassAttributes
      extend Initializer

      # @!method self.attributes_inferrer
      #   @overload attributes_inferrer
      #     @return [Proc]
      #
      #   @overload attributes_inferrer(value)
      #     @param value [Proc]
      #     @return [Proc]
      #
      # @!method self.attr_class
      #   @overload attr_class
      #     @return [Class(ROM::Attribute)]
      #
      #   @overload attr_class(value)
      #     @param value [Class(ROM::Attribute)]
      #     @return [Class(ROM::Attribute)]
      defines :attributes_inferrer, :attr_class

      MissingAttributesError = ::Class.new(::StandardError) do
        def initialize(name, attributes)
          super(
            "Following attributes in #{Relation::Name[name].relation.inspect} schema cannot " \
            "be inferred and have to be defined explicitly: #{attributes.map(&:inspect).join(', ')}"
          )
        end
      end

      DEFAULT_ATTRIBUTES = [EMPTY_ARRAY, EMPTY_ARRAY].freeze

      attributes_inferrer -> * { DEFAULT_ATTRIBUTES }

      attr_class Attribute

      include ::Dry::Equalizer(:options)

      # @!attribute [r] attr_class
      #   @return [Class(ROM::Attribute)]
      option :attr_class, default: -> { self.class.attr_class }

      # @!attribute [r] enabled
      #   @return [Boolean]
      option :enabled, default: -> { true }

      alias_method :enabled?, :enabled

      # @!attribute [r] attributes_inferrer
      #   @return [ROM::Schema::AttributesInferrer]
      option :attributes_inferrer, default: -> { self.class.attributes_inferrer }

      # @api private
      def call(schema, gateway)
        if enabled?
          inferred, missing = attributes_inferrer.(schema, gateway, options)
        else
          inferred, missing = DEFAULT_ATTRIBUTES
        end

        attributes = merge_attributes(schema.attributes, inferred)

        check_all_attributes_defined(schema, attributes, missing)

        { attributes: attributes }
      end

      # @api private
      def check_all_attributes_defined(schema, all_known, not_inferred)
        not_defined = not_inferred - all_known.map(&:name)

        raise MissingAttributesError.new(schema.name, not_defined) unless not_defined.empty?
      end

      # @api private
      # rubocop:disable Metrics/AbcSize
      def merge_attributes(defined, inferred)
        type_lookup = lambda do |attrs, name|
          attrs.find { |a| a.name == name }.type
        end
        defined_with_type, defined_names =
          defined.each_with_object([[], []]) do |attr, (attrs, names)|
            attrs << if attr.type.nil?
                       attr.class.new(
                         type_lookup.(inferred, attr.name),
                         **attr.options
                       )
                     else
                       attr
                     end
            names << attr.name
          end

        defined_with_type + inferred.reject do |attr|
          defined_names.include?(attr.name)
        end
      end
      # rubocop:enable Metrics/AbcSize
    end
  end
end


def merge_attributes(defined, inferred)
        type_lookup = lambda do |attrs, name|
          attrs.find { |a| a.name == name }.type
        end
        defined_with_type, defined_names =
          defined.each_with_object([[], []]) do |attr, (attrs, names)|
            attrs << if attr.type.nil?
                       attr.class.new(
                         type_lookup.(inferred, attr.name),
                         **attr.options
                       )
                     else
                       attr
                     end
            names << attr.name
          end

        defined_with_type + inferred.reject do |attr|
          defined_names.include?(attr.name)
        end
      end


# frozen_string_literal: true

module ROM
  # This extension is only used for environment objects to configure arbitrary options, each
  # adapter can use them according to what they need.
  #
  # @api private
  module Configurable
    class Config
      WRITER_REGEXP = /=$/

      # @!attribute [r] settings
      #   @return [Hash] A hash with defined settings
      attr_reader :settings

      # @api private
      def initialize(settings = {})
        @settings = settings
      end

      # Return a setting
      #
      # @return [Mixed]
      #
      # @api public
      def [](name) = public_send(name)

      # @api private
      def key?(name) = settings.key?(name)

      def to_hash = settings

      # @api private
      def respond_to_missing?(_name, _include_private = false) = true

      # @api private
      def dup = self.class.new(dup_settings(settings))

      private

      def dup_settings(settings)
        settings.to_h do |key, value|
          if value.is_a?(self.class)
            [key, value.dup]
          else
            [key, value]
          end
        end
      end

      # @api private
      def method_missing(meth, *args, &)
        return settings.fetch(meth, nil) if frozen?

        name = meth.to_s
        key = name.gsub(WRITER_REGEXP, '').to_sym

        if writer?(name)
          settings[key] = args.first
        else
          settings.fetch(key) { settings[key] = self.class.new }
        end
      end

      # @api private
      def writer?(name)
        !WRITER_REGEXP.match(name).nil?
      end
    end

    # Return config instance
    #
    # @return [Config]
    #
    # @api private
    def config
      @config ||= Config.new
    end

    # Yield config instance
    #
    # @return [self]
    #
    # @api public
    def configure
      yield(config)
      self
    end
  end
end


# frozen_string_literal: true

module ROM
  # @api private
  module Memoizable
    MEMOIZED_HASH = {}.freeze

    module ClassInterface
      # @api private
      def memoize(*names)
        prepend(Memoizer.new(self, names))
      end

      def new(*, **, &)
        obj = super
        obj.instance_variable_set(:@__memoized__, MEMOIZED_HASH.dup)
        obj
      end
    end

    def self.included(klass)
      super
      klass.extend(ClassInterface)
    end

    attr_reader :__memoized__

    # @api private
    class Memoizer < ::Module
      attr_reader :klass

      attr_reader :names

      # @api private
      def initialize(klass, names)
        super()
        @names = names
        @klass = klass
        define_memoizable_names!
      end

      private

      # @api private
      def define_memoizable_names!
        names.each do |name|
          meth = klass.instance_method(name)

          if meth.parameters.empty?
            define_method(name) do
              __memoized__[name] ||= super()
            end
          else
            define_method(name) do |*args|
              __memoized__[:"#{name}_#{args.hash}"] ||= super(*args)
            end
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/support/inflector'

module ROM
  module ConfigurationDSL
    # Setup DSL-specific command extensions
    #
    # @private
    class Command
      # Generate a command subclass
      #
      # This is used by Setup#commands DSL and its `define` block
      #
      # @api private
      def self.build_class(name, relation, options = EMPTY_HASH, &)
        type = options.fetch(:type) { name }
        command_type = Inflector.classify(type)
        adapter = options.fetch(:adapter)
        parent = ROM::Command.adapter_namespace(adapter).const_get(command_type)
        class_name = generate_class_name(adapter, command_type, relation)

        Dry::Core::ClassBuilder.new(name: class_name, parent: parent).call do |klass|
          klass.register_as(name)
          klass.relation(relation)
          klass.class_eval(&) if block_given?
        end
      end

      # Create a command subclass name based on adapter, type and relation
      #
      # @api private
      def self.generate_class_name(adapter, command_type, relation)
        pieces = ['ROM']
        pieces << Inflector.classify(adapter)
        pieces << 'Commands'
        pieces << "#{command_type}[#{Inflector.classify(relation)}s]"
        pieces.join('::')
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/support/inflector'

module ROM
  module ConfigurationDSL
    # Setup DSL-specific relation extensions
    #
    # @private
    class Relation
      # Generate a relation subclass
      #
      # This is used by Setup#relation DSL
      #
      # @api private
      def self.build_class(name, options = EMPTY_HASH)
        class_name = "ROM::Relation[#{Inflector.camelize(name)}]"
        adapter = options.fetch(:adapter)

        ::Dry::Core::ClassBuilder.new(
          name: class_name, parent: ::ROM::Relation[adapter]
        ).call do |klass|
          klass.gateway(options.fetch(:gateway, :default))
          klass.schema(name) { nil }
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'concurrent/map'

module ROM
  class Relation
    # Relation name container
    #
    # This is a simple struct with two fields.
    # It handles both relation registration name (i.e. Symbol) and dataset name.
    # The reason we need it is a simplification of passing around these two objects.
    # It is quite common to have a dataset named differently from a relation
    # built on top if you are dealing with a legacy DB and often you need both
    # to support things such as associations (rom-sql as an example).
    #
    # @api private
    class Name
      include ::Dry::Equalizer(:relation, :dataset, :key)

      # Coerce an object to a Name instance
      #
      # @return [ROM::Relation::Name]
      #
      # @api private
      def self.[](*args)
        cache.fetch_or_store(args.hash) do
          relation, dataset, aliaz = args

          if relation.is_a?(Name)
            relation
          else
            new(relation, dataset, aliaz)
          end
        end
      end

      # @api private
      def self.cache
        @cache ||= Concurrent::Map.new
      end

      # Relation registration name
      #
      # @return [Symbol]
      #
      # @api private
      attr_reader :relation

      # Underlying dataset name
      #
      # @return [Symbol]
      #
      # @api private
      attr_reader :dataset

      attr_reader :aliaz
      attr_reader :key

      # @api private
      def initialize(relation, dataset = relation, aliaz = nil)
        @relation = relation
        @dataset = dataset || relation
        @key = aliaz || relation
        @aliaz = aliaz
      end

      # @api private
      def as(aliaz) = self.class[relation, dataset, aliaz]

      # @api private
      def aliased? = aliaz && aliaz != relation

      # Return relation name
      #
      # @return [String]
      #
      # @api private
      def to_s
        if aliased?
          "#{relation} on #{dataset} as #{aliaz}"
        elsif relation == dataset
          relation.to_s
        else
          "#{relation} on #{dataset}"
        end
      end

      # Alias for registration key implicitly called by ROM::Registry
      #
      # @return [Symbol]
      #
      # @api private
      def to_sym = relation

      # Return inspected relation
      #
      # @return [String]
      #
      # @api private
      def inspect = "#{self.class.name}(#{self})"
    end
  end
end


# frozen_string_literal: true

module ROM
  class Relation
    # Extensions for relation classes which provide access to commands
    #
    # @api public
    module Commands
      # Return a command for the relation
      #
      # This method can either return an existing custom command identified
      # by `type` param, or generate a command dynamically based on relation
      # AST.
      #
      # @example build a simple :create command
      #   users.command(:create)
      #
      # @example build a command which returns multiple results
      #   users.command(:create, result: many)
      #
      # @example build a command which uses a specific plugin
      #   users.command(:create, use: :timestamps)
      #
      # @example build a command which sends results through a custom mapper
      #   users.command(:create, mapper: :my_mapper_identifier)
      #
      # @example return an existing custom command
      #   users.command(:my_custom_command_identifier)
      #
      # @param type [Symbol] The command type (:create, :update or :delete)
      # @param opts [Hash] Additional options
      # @option opts [Symbol] :mapper (nil) An optional mapper applied to the command result
      # @option opts [Array<Symbol>] :use ([]) A list of command plugins
      # @option opts [Symbol] :result (:one) Set how many results the command should return.
      #                                       Can be `:one` or `:many`
      #
      # @return [ROM::Command]
      #
      # @api public
      def command(type, mapper: nil, use: EMPTY_ARRAY, plugins_options: EMPTY_HASH, **opts)
        base_command =
          if commands.key?(type)
            commands[type]
          else
            commands[type, adapter, to_ast, use, plugins_options, opts]
          end

        command =
          if mapper
            base_command >> mappers[mapper]
          elsif auto_map?
            base_command >> self.mapper
          else
            base_command
          end

        if command.restrictible?
          command.new(self)
        else
          command
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/types'
require 'rom/initializer'
require 'rom/pipeline'
require 'rom/relation/name'
require 'rom/relation/materializable'
require 'rom/relation/graph'
require 'rom/relation/wrap'
require 'rom/relation/composite'

module ROM
  class Relation
    # Curried relation is a special relation proxy used by auto-curry mechanism.
    #
    # When a relation view method is called without all arguments, a curried proxy
    # is returned that can be fully applied later on.
    #
    # Curried relations are typically used for relation composition
    #
    # @api public
    class Curried
      extend Initializer

      WRAPS = [Relation, Graph, Wrap, Composite].freeze

      include Dry::Equalizer(:relation, :options)
      include Materializable
      include Pipeline

      undef :map_with

      # @!attribute [r] relation
      #   @return [Relation] The source relation that is curried
      param :relation

      # @!attribute [r] view
      #   @return [Symbol] The name of relation's view method
      option :view, type: Types::Strict::Symbol

      # @!attribute [r] arity
      #   @return [Integer] View's arity
      option :arity, type: Types::Strict::Integer

      # @!attribute [r] curry_args
      #   @return [Array] Arguments that will be passed to curried view
      option :curry_args, default: -> { EMPTY_ARRAY }

      # Load relation if args match the arity
      #
      # @return [Loaded,Curried]
      #
      # @api public
      def call(*args)
        all_args = curry_args + args

        if all_args.empty?
          raise ArgumentError,
                "curried #{relation.class}##{view} relation was called without any arguments"
        end

        if args.empty?
          self
        elsif arity == all_args.size
          Loaded.new(relation.__send__(view, *all_args))
        else
          __new__(relation, curry_args: all_args)
        end
      end
      alias_method :[], :call

      # Relations are coercible to an array but a curried relation cannot be coerced
      # When something tries to do this, an exception will be raised
      #
      # @raise ArgumentError
      #
      # @api public
      def to_a
        raise(
          ArgumentError,
          "#{relation.class}##{view} arity is #{arity} " \
          "(#{curry_args.size} args given)"
        )
      end
      alias_method :to_ary, :to_a

      # Return if this lazy relation is curried
      #
      # @return [true]
      #
      # @api private
      def curried? = true

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

      private

      # @api private
      def __new__(relation, **new_opts)
        self.class.new(relation, **options, **new_opts)
      end

      # @api private
      def composite_class = Relation::Composite

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

          super if response.is_a?(self.class)

          if WRAPS.any? { |klass| response.is_a?(klass) }
            __new__(response)
          else
            response
          end
        else
          super
        end
      end
    end
  end
end


def method_missing(meth, ...)
        if relation.respond_to?(meth)
          response = relation.__send__(meth, ...)

          super if response.is_a?(self.class)

          if WRAPS.any? { |klass| response.is_a?(klass) }
            __new__(response)
          else
            response
          end
        else
          super
        end
      end


# frozen_string_literal: true

require 'set'

require 'rom/support/inflector'

require 'rom/constants'
require 'rom/relation/name'
require 'rom/relation/view_dsl'
require 'rom/schema'
require 'rom/support/notifications'

module ROM
  class Relation
    # Global class-level API for relation classes
    #
    # @api public
    # rubocop:disable Metrics/ModuleLength
    module ClassInterface
      extend Notifications::Listener

      subscribe('configuration.relations.object.registered') do |event|
        relation = event[:relation]
        registry = event[:registry]

        schemas = relation.schemas.reduce({}) do |h, (a, e)|
          h.update(a => e.is_a?(Proc) ? relation.class.instance_exec(registry, &e) : e)
        end

        relation.schemas.update(schemas)
        relation
      end

      DEFAULT_DATASET_PROC = -> * { self }.freeze
      INVALID_RELATIONS_NAMES = [
        :relations
      ].freeze

      # Return adapter-specific relation subclass
      #
      # @example
      #   ROM::Relation[:memory]
      #   # => ROM::Memory::Relation
      #
      # @return [Class]
      #
      # @api public
      def [](adapter)
        ROM.adapters.fetch(adapter).const_get(:Relation)
      rescue KeyError
        raise AdapterNotPresentError.new(adapter, :relation)
      end

      # Set or get custom dataset block
      #
      # This block will be evaluated when a relation is instantiated and registered
      # in a relation registry.
      #
      # @example
      #   class Users < ROM::Relation[:memory]
      #     dataset { sort_by(:id) }
      #   end
      #
      # @api public
      def dataset(&block)
        if defined?(@dataset)
          @dataset
        else
          @dataset = block || DEFAULT_DATASET_PROC
        end
      end

      # Specify canonical schema for a relation
      #
      # With a schema defined commands will set up a type-safe input handler
      # automatically
      #
      # @example
      #   class Users < ROM::Relation[:sql]
      #     schema do
      #       attribute :id, Types::Serial
      #       attribute :name, Types::String
      #     end
      #   end
      #
      #   # access schema from a finalized relation
      #   users.schema
      #
      # @return [Schema]
      #
      # @param [Symbol] dataset An optional dataset name
      # @param [Boolean] infer Whether to do an automatic schema inferring
      #
      # @api public
      # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
      def schema(dataset = nil, as: nil, infer: false, &)
        if defined?(@schema) && !block_given? && !infer
          @schema
        elsif block_given? || infer
          raise MissingSchemaClassError, self unless schema_class

          ds_name = dataset || schema_opts.fetch(:dataset, default_name.dataset)
          relation = as || schema_opts.fetch(:relation, ds_name)

          raise InvalidRelationName, relation if invalid_relation_name?(relation)

          @relation_name = Name[relation, ds_name]

          @schema_proc = proc do |*args, &inner_block|
            schema_dsl.new(
              relation_name,
              schema_class: schema_class,
              attr_class: schema_attr_class,
              inferrer: schema_inferrer.with(enabled: infer),
              &
            ).call(*args, &inner_block)
          end
        end
      end
      # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity

      # Assign a schema to a relation class
      #
      # @param [Schema] schema
      #
      # @return [Schema]
      #
      # @api private
      def set_schema!(schema)
        @schema = schema
      end

      # @api private
      attr_reader :schema_proc

      # @!attribute [r] relation_name
      #   @return [Name] Qualified relation name
      def relation_name
        raise MissingSchemaError, self unless defined?(@relation_name)

        @relation_name
      end

      # Define a relation view with a specific schema
      #
      # This method should only be used in cases where a given adapter doesn't
      # support automatic schema projection at run-time.
      #
      # **It's not needed in rom-sql**
      #
      # @overload view(name, schema, &block)
      #   @example View with the canonical schema
      #     class Users < ROM::Relation[:sql]
      #       view(:listing, schema) do
      #         order(:name)
      #       end
      #     end
      #
      #   @example View with a projected schema
      #     class Users < ROM::Relation[:sql]
      #       view(:listing, schema.project(:id, :name)) do
      #         order(:name)
      #       end
      #     end
      #
      # @overload view(name, &block)
      #   @example View with the canonical schema and arguments
      #     class Users < ROM::Relation[:sql]
      #       view(:by_name) do |name|
      #         where(name: name)
      #       end
      #     end
      #
      #   @example View with projected schema and arguments
      #     class Users < ROM::Relation[:sql]
      #       view(:by_name) do
      #         schema { project(:id, :name) }
      #         relation { |name| where(name: name) }
      #       end
      #     end
      #
      #   @example View with a schema extended with foreign attributes
      #     class Users < ROM::Relation[:sql]
      #       view(:index) do
      #         schema { append(relations[:tasks][:title]) }
      #         relation { |name| where(name: name) }
      #       end
      #     end
      #
      # @return [Symbol] view method name
      #
      # @api public
      # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
      def view(*args, &block)
        if args.size == 1 && block.arity.positive?
          raise ArgumentError, 'schema attribute names must be provided as the second argument'
        end

        name, new_schema_fn, relation_block =
          if args.size == 1
            ViewDSL.new(*args, schema, &block).call
          else
            [*args, block]
          end

        schemas[name] =
          if args.size == 2
            -> _ { schema.project(*args[1]) }
          else
            new_schema_fn
          end

        if relation_block.arity.positive?
          auto_curry_guard do
            define_method(name, &relation_block)

            auto_curry(name) do
              schemas[name].(self)
            end
          end
        else
          define_method(name) do
            schemas[name].(instance_exec(&relation_block))
          end
        end

        name
      end
      # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

      # Dynamically define a method that will forward to the dataset and wrap
      # response in the relation itself
      #
      # @example
      #   class SomeAdapterRelation < ROM::Relation
      #     forward :super_query
      #   end
      #
      # @api public
      def forward(*methods)
        methods.each do |method|
          class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def #{method}(...)                        # def super_query(...)
              new(dataset.__send__(:#{method}, ...))  #   new(dataset.__send__(:super_query, ...))
            end                                       # end
          RUBY
        end
      end

      # Include a registered plugin in this relation class
      #
      # @param [Symbol] plugin
      # @param [Hash] options
      # @option options [Symbol] :adapter (:default) first adapter to check for plugin
      #
      # @api public
      def use(plugin, **options)
        ROM.plugin_registry[:relation].fetch(plugin, adapter).apply_to(self, **options)
      end

      # Build default mapper registry
      #
      # @return [MapperRegistry]
      #
      # @api private
      def mapper_registry(opts = EMPTY_HASH)
        adapter_ns = ROM.adapters[adapter]

        compiler =
          if adapter_ns&.const_defined?(:MapperCompiler)
            adapter_ns.const_get(:MapperCompiler)
          else
            MapperCompiler
          end

        MapperRegistry.new({}, compiler: compiler.new(**opts), **opts)
      end

      # @api private
      def curried = Curried

      # @api private
      def view_methods
        ancestor_methods = ancestors.reject { |klass| klass == self }
          .map(&:instance_methods).flatten(1)

        instance_methods - ancestor_methods + auto_curried_methods.to_a
      end

      # @api private
      def schemas
        @schemas ||= {}
      end

      # Return default relation name used in schemas
      #
      # @return [Name]
      #
      # @api private
      def default_name
        Name[Inflector.underscore(name).tr('/', '_').to_sym]
      end

      # @api private
      def default_schema(klass = self)
        klass.schema ||
          if klass.schema_proc
            klass.set_schema!(klass.schema_proc.call)
          else
            klass.schema_class.define(klass.default_name)
          end
      end

      # @api private
      def name = super || superclass.name

      private

      def invalid_relation_name?(relation)
        INVALID_RELATIONS_NAMES.include?(relation.to_sym)
      end
    end
    # rubocop:enable Metrics/ModuleLength
  end
end


def view(*args, &block)
        if args.size == 1 && block.arity.positive?
          raise ArgumentError, 'schema attribute names must be provided as the second argument'
        end

        name, new_schema_fn, relation_block =
          if args.size == 1
            ViewDSL.new(*args, schema, &block).call
          else
            [*args, block]
          end

        schemas[name] =
          if args.size == 2
            -> _ { schema.project(*args[1]) }
          else
            new_schema_fn
          end

        if relation_block.arity.positive?
          auto_curry_guard do
            define_method(name, &relation_block)

            auto_curry(name) do
              schemas[name].(self)
            end
          end
        else
          define_method(name) do
            schemas[name].(instance_exec(&relation_block))
          end
        end

        name
      end


def mapper_registry(opts = EMPTY_HASH)
        adapter_ns = ROM.adapters[adapter]

        compiler =
          if adapter_ns&.const_defined?(:MapperCompiler)
            adapter_ns.const_get(:MapperCompiler)
          else
            MapperCompiler
          end

        MapperRegistry.new({}, compiler: compiler.new(**opts), **opts)
      end


# frozen_string_literal: true

module ROM
  class Relation
    # Materializes a relation and exposes interface to access the data.
    #
    # This relation type is returned when a lazy relation is called
    #
    # @api public
    class Loaded
      include ::Enumerable
      include ::Dry::Equalizer(:source, :collection)

      # Coerce loaded relation to an array
      #
      # @return [Array]
      #
      # @api public
      alias_method :to_ary, :to_a

      # Source relation
      #
      # @return [Relation]
      #
      # @api private
      attr_reader :source

      # Materialized relation
      #
      # @return [Object]
      #
      # @api private
      attr_reader :collection

      # @api private
      def initialize(source, collection = source.to_a)
        @source = source
        @collection = collection
      end

      # Yield relation tuples
      #
      # @yield [Hash]
      #
      # @api public
      def each(&)
        return to_enum unless block_given?

        collection.each(&)
      end

      # Returns a single tuple from the relation if there is one.
      #
      # @raise [ROM::TupleCountMismatchError] if the relation contains more than
      #   one tuple
      #
      # @api public
      def one
        if collection.count > 1
          raise(
            TupleCountMismatchError,
            'The relation consists of more than one tuple'
          )
        else
          collection.first
        end
      end

      # Like [one], but additionally raises an error if the relation is empty.
      #
      # @raise [ROM::TupleCountMismatchError] if the relation does not contain
      #   exactly one tuple
      #
      # @api public
      def one!
        one || raise(
          TupleCountMismatchError,
          'The relation does not contain any tuples'
        )
      end

      # Return a list of values under provided key
      #
      # @example
      #   all_users = rom.relations[:users].call
      #   all_users.pluck(:name)
      #   # ["Jane", "Joe"]
      #
      # @param [Symbol] key The key name
      #
      # @return [Array]
      #
      # @raise KeyError when provided key doesn't exist in any of the tuples
      #
      # @api public
      def pluck(key) = map { |tuple| tuple.fetch(key) }

      # Pluck primary key values
      #
      # This method *may not work* with adapters that don't provide relations
      # that have primary key configured
      #
      # @example
      #   users = rom.relations[:users].call
      #   users.primary_keys
      #   # [1, 2, 3]
      #
      # @return [Array]
      #
      # @api public
      def primary_keys = pluck(source.primary_key)

      # Return if loaded relation is empty
      #
      # @return [TrueClass,FalseClass]
      #
      # @api public
      def empty? = collection.empty?

      # Return a loaded relation with a new collection
      #
      # @api public
      def new(collection) = self.class.new(source, collection)
    end
  end
end


# frozen_string_literal: true

require 'rom/relation/graph'
require 'rom/relation/commands'

module ROM
  class Relation
    # Represents a relation graphs which combines root relation
    # with other relation nodes
    #
    # @api public
    class Combined < Graph
      include Commands

      # Create a new relation combined with others
      #
      # @param [Relation] relation
      # @param [Array<Relation>] nodes
      #
      # @return [Combined]
      #
      # @api public
      def self.new(relation, nodes)
        struct_ns = relation.options[:struct_namespace]
        new_nodes = nodes.uniq(&:name).map { |node| node.struct_namespace(struct_ns) }

        root =
          if relation.is_a?(self)
            new_nodes.concat(relation.nodes)
            relation.root
          else
            relation
          end

        super(root, new_nodes)
      end

      # Combine this graph with more nodes
      #
      # @param [Array<Relation>] others A list of relations
      #
      # @return [Graph]
      #
      # @api public
      def combine_with(*others)
        self.class.new(root, nodes + others)
      end

      # Combine with other relations
      #
      # @see Relation#combine
      #
      # @return [Combined]
      #
      # @api public
      def combine(*args)
        self.class.new(root, nodes + root.combine(*args).nodes)
      end

      # Materialize combined relation
      #
      # @return [Loaded]
      #
      # @api public
      def call(*args)
        left = root.with(auto_map: false, auto_struct: false).call(*args)

        right =
          if left.empty?
            nodes.map { |node| Loaded.new(node, EMPTY_ARRAY) }
          else
            nodes.map { |node| node.call(left) }
          end

        if auto_map?
          Loaded.new(self, mapper.([left, right]))
        else
          Loaded.new(self, [left, right])
        end
      end

      # Return a new combined relation with adjusted node returned from a block
      #
      # @example with a node identifier
      #   combine(:tasks).node(:tasks) { |tasks| tasks.prioritized }
      #
      # @example with a nested path
      #   combine(tasks: :tags).node(tasks: :tags) { |tags| tags.where(name: 'red') }
      #
      # @param [Symbol] name The node relation name
      #
      # @yieldparam [Relation] relation The relation node
      # @yieldreturn [Relation] The new relation node
      #
      # @return [Relation]
      #
      # @api public
      # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
      def node(name, &)
        if name.is_a?(Symbol) && !nodes.map { |n| n.name.key }.include?(name)
          raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
        end

        new_nodes = nodes.map { |node|
          case name
          when Symbol
            name == node.name.key ? yield(node) : node
          when Hash
            other, *rest = name.flatten(1)
            if other == node.name.key
              nodes.detect { |n| n.name.key == other }.node(*rest, &)
            else
              node
            end
          else
            node
          end
        }

        with_nodes(new_nodes)
      end
      # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity

      # Return a `:create` command that can insert data from a nested hash.
      #
      # This is limited to `:create` commands only, because automatic restriction
      # for `:update` commands would be quite complex. It's possible that in the
      # future support for `:update` commands will be added though.
      #
      # Another limitation is that it can only work when you're composing
      # parent and its child(ren), which follows canonical hierarchy from your
      # database, so that parents are created first, then their PKs are set
      # as FKs in child tuples. It should be possible to make it work with
      # both directions (parent => child or child => parent), and it would
      # require converting input tuples based on how they depend on each other,
      # which we could do in the future.
      #
      # Expanding functionality of this method is planned for rom 5.0.
      #
      # @see Relation#command
      #
      # @raise NotImplementedError when type is not `:create`
      #
      # @api public
      def command(type, **)
        if type == :create
          super
        else
          raise NotImplementedError,
                "#{self.class}#command doesn't work with #{type.inspect} command type yet"
        end
      end

      private

      # @api private
      def decorate?(other)
        super || other.is_a?(self.class) || other.is_a?(Wrap)
      end
    end
  end
end


def call(*args)
        left = root.with(auto_map: false, auto_struct: false).call(*args)

        right =
          if left.empty?
            nodes.map { |node| Loaded.new(node, EMPTY_ARRAY) }
          else
            nodes.map { |node| node.call(left) }
          end

        if auto_map?
          Loaded.new(self, mapper.([left, right]))
        else
          Loaded.new(self, [left, right])
        end
      end


def node(name, &)
        if name.is_a?(Symbol) && !nodes.map { |n| n.name.key }.include?(name)
          raise ArgumentError, "#{name.inspect} is not a valid aggregate node name"
        end

        new_nodes = nodes.map { |node|
          case name
          when Symbol
            name == node.name.key ? yield(node) : node
          when Hash
            other, *rest = name.flatten(1)
            if other == node.name.key
              nodes.detect { |n| n.name.key == other }.node(*rest, &)
            else
              node
            end
          else
            node
          end
        }

        with_nodes(new_nodes)
      end


# frozen_string_literal: true

require 'pathname'

require 'rom/support/inflector'
require 'rom/types'
require 'rom/setup/auto_registration_strategies/base'

module ROM
  module AutoRegistrationStrategies
    # Custom namespace strategy loads components and assumes they are defined
    # within the provided namespace
    #
    # @api private
    class CustomNamespace < Base
      # @!attribute [r] directory
      #   @return [Pathname] The path to dir with components
      option :directory, type: PathnameType

      # @!attribute [r] namespace
      #   @return [String] Name of a namespace
      option :namespace, type: Types::Strict::String

      # Loads components
      #
      # @api private
      def call
        parts = path_arr.map { |part| Inflector.camelize(part) }
        potential = parts.map.with_index do |_, i|
          parts[(i - parts.size)..parts.size]
        end
        attempted = []

        potential.map do |path|
          const_fragment = path.join('::')

          constant = "#{namespace}::#{const_fragment}"

          return constant if ns_const.const_defined?(const_fragment)

          attempted << constant
        end

        # If we have reached this point, its means constant is not defined and
        # NameError will be thrown if we attempt to camelize something like:
        # `"#{namespace}::#{Inflector.camelize(filename)}"`
        # so we can assume naming convention was not respected in required
        # file.

        raise NameError, name_error_message(attempted)
      end

      private

      # @api private
      def name_error_message(attempted)
        'required file does not define expected constant name; either ' \
          'register your constant explicitly of try following the path' \
          "naming convention like:\n\n\t- #{attempted.join("\n\t- ")}\n"
      end

      # @api private
      def filename = Pathname(file).basename('.rb')

      # @api private
      def ns_const
        @namespace_constant ||= Inflector.constantize(namespace)
      end

      # @api private
      def path_arr = file_path << filename

      # @api private
      def file_path
        File.dirname(file).split('/') - directory.to_s.split('/')
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/constants'
require 'rom/relation_registry'
require 'rom/mapper_registry'

module ROM
  class Finalize
    class FinalizeRelations
      attr_reader :notifications

      class RegistryReaders < ::Module
        def initialize(relations)
          super()

          relations.each do |name|
            define_method(name) { __registry__[name] }
          end
        end
      end

      # Build relation registry of specified descendant classes
      #
      # This is used by the setup
      #
      # @param [Hash] gateways
      # @param [Array] relation_classes a list of relation descendants
      #
      # @api private
      def initialize(gateways, relation_classes, notifications:, mappers: nil, plugins: EMPTY_ARRAY)
        @gateways = gateways
        @relation_classes = relation_classes
        @mappers = mappers
        @plugins = plugins
        @notifications = notifications
      end

      # @return [Hash]
      #
      # @api private
      #
      # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      def run!
        relation_registry = RelationRegistry.new do |registry, relations|
          registry_readers = RegistryReaders.new(relation_names)
          @relation_classes.each do |klass|
            unless klass.adapter
              raise MissingAdapterIdentifierError,
                    "Relation class +#{klass}+ is missing the adapter identifier"
            end

            key = klass.relation_name.to_sym

            if registry.key?(key)
              raise RelationAlreadyDefinedError,
                    "Relation with name #{key.inspect} registered more than once"
            end

            klass.use(:registry_reader, readers: registry_readers)

            notifications.trigger(
              'configuration.relations.class.ready',
              relation: klass,
              adapter: klass.adapter
            )

            relations[key] = build_relation(klass, registry)
          end

          registry.each_value do |relation|
            notifications.trigger(
              'configuration.relations.object.registered',
              relation: relation, registry: registry
            )
          end
        end

        notifications.trigger(
          'configuration.relations.registry.created', registry: relation_registry
        )

        relation_registry
      end
      # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

      # @return [ROM::Relation]
      #
      # @api private
      #
      # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
      def build_relation(klass, registry)
        # TODO: raise a meaningful error here and add spec covering the case
        #       where klass' gateway points to non-existant repo
        gateway = @gateways.fetch(klass.gateway)

        plugins = schema_plugins

        schema = klass.schema_proc.call do
          plugins.each { |plugin| app_plugin(plugin) }
        end

        klass.set_schema!(schema) if klass.schema.nil?

        notifications.trigger(
          'configuration.relations.schema.allocated',
          schema: schema, gateway: gateway, registry: registry
        )

        relation_plugins.each do |plugin|
          plugin.apply_to(klass)
        end

        notifications.trigger(
          'configuration.relations.schema.set',
          schema: schema, relation: klass, registry: registry, adapter: klass.adapter
        )

        rel_key = schema.name.to_sym
        dataset = gateway.dataset(schema.name.dataset).instance_exec(klass, &klass.dataset)

        notifications.trigger(
          'configuration.relations.dataset.allocated',
          dataset: dataset, relation: klass, adapter: klass.adapter
        )

        options = {
          __registry__: registry,
          mappers: mapper_registry(rel_key, klass),
          schema: schema,
          **plugin_options
        }

        klass.new(dataset, **options)
      end
      # rubocop:enable Metrics/MethodLength, Metrics/AbcSize

      # @api private
      def mapper_registry(rel_key, rel_class)
        registry = rel_class.mapper_registry(cache: @mappers.cache)

        if @mappers.key?(rel_key)
          registry.merge(@mappers[rel_key])
        else
          registry
        end
      end

      # @api private
      def plugin_options
        relation_plugins.map(&:config).map(&:to_hash).reduce(:merge) || EMPTY_HASH
      end

      # @api private
      def relation_plugins
        @plugins.select { |p| p.type == :relation }
      end

      # @api private
      def schema_plugins
        @plugins.select { |p| p.type == :schema }
      end

      # @api private
      def relation_names
        @relation_classes.map(&:relation_name).map(&:relation).uniq
      end
    end
  end
end


def build_relation(klass, registry)
        # TODO: raise a meaningful error here and add spec covering the case
        #       where klass' gateway points to non-existant repo
        gateway = @gateways.fetch(klass.gateway)

        plugins = schema_plugins

        schema = klass.schema_proc.call do
          plugins.each { |plugin| app_plugin(plugin) }
        end

        klass.set_schema!(schema) if klass.schema.nil?

        notifications.trigger(
          'configuration.relations.schema.allocated',
          schema: schema, gateway: gateway, registry: registry
        )

        relation_plugins.each do |plugin|
          plugin.apply_to(klass)
        end

        notifications.trigger(
          'configuration.relations.schema.set',
          schema: schema, relation: klass, registry: registry, adapter: klass.adapter
        )

        rel_key = schema.name.to_sym
        dataset = gateway.dataset(schema.name.dataset).instance_exec(klass, &klass.dataset)

        notifications.trigger(
          'configuration.relations.dataset.allocated',
          dataset: dataset, relation: klass, adapter: klass.adapter
        )

        options = {
          __registry__: registry,
          mappers: mapper_registry(rel_key, klass),
          schema: schema,
          **plugin_options
        }

        klass.new(dataset, **options)
      end


# frozen_string_literal: true

require 'rom/registry'
require 'rom/command_compiler'
require 'rom/command_registry'

module ROM
  class Finalize
    class FinalizeCommands
      attr_reader :notifications

      # Build command registry hash for provided relations
      #
      # @param [RelationRegistry] relations registry
      # @param [Hash] gateways
      # @param [Array] command_classes a list of command subclasses
      #
      # @api private
      def initialize(relations, gateways, command_classes, notifications)
        @relations = relations
        @gateways = gateways
        @command_classes = command_classes
        @notifications = notifications
      end

      # @return [Hash]
      #
      # @api private
      #
      # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      def run!
        commands = @command_classes.map do |klass|
          relation = @relations[klass.relation]
          gateway = @gateways[relation.gateway]

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

          klass.extend_for_relation(relation) if klass.restrictable

          klass.build(relation)
        end

        registry = Registry.new
        compiler = CommandCompiler.new(@gateways, @relations, registry, notifications)

        @relations.each do |(name, relation)|
          rel_commands = commands.select { |c| c.relation.name == relation.name }

          rel_commands.each do |command|
            identifier = command.class.register_as || command.class.default_name
            relation.commands.elements[identifier] = command
          end

          relation.commands.set_compiler(compiler)
          relation.commands.set_mappers(relation.mappers)

          registry.elements[name] = relation.commands
        end

        registry
      end
      # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
    end
  end
end


# frozen_string_literal: true

require 'rom/registry'

module ROM
  class Finalize
    class FinalizeMappers
      attr_reader :mapper_classes
      attr_reader :mapper_objects
      attr_reader :registry_hash

      # @api private
      def initialize(mapper_classes, mapper_objects)
        @mapper_classes = mapper_classes
        @mapper_objects = mapper_objects

        check_duplicate_registered_mappers

        @registry_hash = [@mapper_classes.map(&:base_relation) + @mapper_objects.keys]
          .flatten
          .uniq
          .to_h { [_1, {}] }
      end

      # @api private
      def run!
        cache = Cache.new

        mappers = registry_hash.to_h do |relation_name, relation_mappers|
          relation_mappers.update(build_mappers(relation_name))

          if mapper_objects.key?(relation_name)
            relation_mappers.update(mapper_objects[relation_name])
          end

          [relation_name, MapperRegistry.new(relation_mappers, cache: cache)]
        end

        Registry.new(mappers, cache: cache)
      end

      private

      def check_duplicate_registered_mappers
        duplicates = mapper_classes.map { [_1.relation, _1.register_as] }.tally.select { _2 > 1 }

        case duplicates.first
        in [rel, as], _
          raise MapperAlreadyDefinedError,
                "Mapper with `register_as #{as.inspect}` registered more " \
                "than once for relation #{rel.inspect}"
        else
          nil
        end
      end

      def build_mappers(relation_name)
        mapper_classes
          .select { |klass| klass.base_relation == relation_name }
          .to_h { |k| [k.register_as || k.relation, k.build] }
      end
    end
  end
end


def check_duplicate_registered_mappers
        duplicates = mapper_classes.map { [_1.relation, _1.register_as] }.tally.select { _2 > 1 }

        case duplicates.first
        in [rel, as], _
          raise MapperAlreadyDefinedError,
                "Mapper with `register_as #{as.inspect}` registered more " \
                "than once for relation #{rel.inspect}"
        else
          nil
        end
      end


# frozen_string_literal: true

module ROM
  module Commands
    class Lazy
      # Lazy command wrapper for delete commands
      #
      # @api public
      class Delete < Lazy
        # Execute a lazy delete command
        #
        # @see Commands::Delete#call
        #
        # @return [Hash, Array<Hash>]
        #
        # @api public
        def call(*args)
          first = args.first
          last = args.last
          size = args.size

          if size > 1 && last.is_a?(Array)
            raise NotImplementedError
          else
            input = evaluator.call(first)

            if input.is_a?(Array)
              input.map do |item|
                command_proc[command, *(size > 1 ? [last, item] : [input])].call
              end
            else
              command_proc[command, input].call
            end
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

module ROM
  module Commands
    class Lazy
      # Lazy command wrapper for create commands
      #
      # @api public
      class Create < Lazy
        # Execute a command
        #
        # @see Command::Create#call
        #
        # @return [Hash,Array<Hash>]
        #
        # @api public
        # rubocop:disable Metrics/AbcSize
        def call(*args)
          first = args.first
          last = args.last
          size = args.size

          if size > 1 && last.is_a?(Array)
            last.map.with_index do |parent, index|
              children = evaluator.call(first, index)
              command_proc[command, parent, children].call(children, parent)
            end.reduce(:concat)
          else
            input = evaluator.call(first)
            command.call(input, *args[1..size - 1])
          end
        end
        # rubocop:enable Metrics/AbcSize
      end
    end
  end
end


# frozen_string_literal: true

module ROM
  module Commands
    class Lazy
      # Lazy command wrapper for update commands
      #
      # @api public
      class Update < Lazy
        # Execute a lazy update command
        #
        # @see Commands::Update#call
        #
        # @return [Hash, Array<Hash>]
        #
        # @api public
        # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
        def call(*args)
          first = args.first
          last = args.last
          size = args.size

          if size > 1 && last.is_a?(Array)
            last.map.with_index do |parent, index|
              children = evaluator.call(first, index)

              children.map do |child|
                command_proc[command, parent, child].call(child, parent)
              end
            end.reduce(:concat)
          else
            input = evaluator.call(first)

            if input.is_a?(Array)
              input.map.with_index do |item, _index|
                command_proc[command, last, item].call(item, *args[1..size - 1])
              end
            else
              command_proc[command, *(size > 1 ? [last, input] : [input])]
                .call(input, *args[1..size - 1])
            end
          end
        end
        # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/commands/graph/input_evaluator'

module ROM
  module Commands
    class Graph
      # Class methods for command Graph
      #
      # @api private
      module ClassInterface
        # Build a command graph recursively
        #
        # This is used by `Container#command` when array with options is passed in
        #
        # @param [Registry] registry The command registry from container
        # @param [Array] options The options array
        # @param [Array] path The path for input evaluator proc
        #
        # @return [Graph]
        #
        # @api private
        def build(registry, options, path = EMPTY_ARRAY)
          options.reduce { |spec, other| build_command(registry, spec, other, path) }
        end

        # @api private
        # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
        def build_command(registry, spec, other, path)
          cmd_opts, nodes = other

          key, relation =
            if spec.is_a?(Hash)
              spec.to_a.first
            else
              [spec, spec]
            end

          name, opts =
            if cmd_opts.is_a?(Hash)
              cmd_opts.to_a.first
            else
              [cmd_opts]
            end

          command = registry[relation][name]
          tuple_path = [*path] << key
          input_proc = InputEvaluator.build(tuple_path, nodes)

          command = command.curry(input_proc, opts)

          if nodes
            if nodes.all? { |node| node.is_a?(Array) }
              command.combine(*nodes.map { |node| build(registry, node, tuple_path) })
            else
              command.combine(build(registry, nodes, tuple_path))
            end
          else
            command
          end
        end
        # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
      end
    end
  end
end


# frozen_string_literal: true

module ROM
  module Commands
    class Graph
      # Evaluator for lazy commands which extracts values for commands from nested hashes
      #
      # @api private
      class InputEvaluator
        include Dry::Equalizer(:tuple_path, :excluded_keys)

        # @!attribute [r] tuple_path
        #   @return [Array<Symbol>] A list of keys pointing to a value inside a hash
        attr_reader :tuple_path

        # @!attribute [r] excluded_keys
        #   @return [Array<Symbol>] A list of keys that should be excluded
        attr_reader :excluded_keys

        # @!attribute [r] exclude_proc
        #   @return [Array<Symbol>] A function that should determine which keys should be excluded
        attr_reader :exclude_proc

        # Build an input evaluator
        #
        # @param [Array<Symbol>] tuple_path The tuple path
        # @param [Array] nodes
        #
        # @return [InputEvaluator]
        #
        # @api private
        def self.build(tuple_path, nodes)
          new(tuple_path, extract_excluded_keys(nodes))
        end

        # @api private
        def self.extract_excluded_keys(nodes)
          return unless nodes

          nodes
            .map { |item| item.is_a?(Array) && item.size > 1 ? item.first : item }
            .compact
            .map { |item| item.is_a?(Hash) ? item.keys.first : item }
            .reject { |item| item.is_a?(Array) }
        end

        # Return default exclude_proc
        #
        # @api private
        def self.exclude_proc(excluded_keys)
          -> input { input.except(*excluded_keys) }
        end

        # Initialize a new input evaluator
        #
        # @return [InputEvaluator]
        #
        # @api private
        def initialize(tuple_path, excluded_keys)
          @tuple_path = tuple_path
          @excluded_keys = excluded_keys
          @exclude_proc = self.class.exclude_proc(excluded_keys)
        end

        # Evaluate input hash
        #
        # @param [Hash] input The input hash
        # @param [Integer] index Optional index
        #
        # @return [Hash]
        # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
        def call(input, index = nil)
          value =
            begin
              if index
                tuple_path[0..tuple_path.size - 2]
                  .reduce(input) { |a, e| a.fetch(e) }
                  .at(index)[tuple_path.last]
              else
                tuple_path.reduce(input) { |a, e| a.fetch(e) }
              end
            rescue KeyError => e
              raise KeyMissing, e.message
            end

          if excluded_keys
            value.is_a?(Array) ? value.map(&exclude_proc) : exclude_proc[value]
          else
            value
          end
        end
        # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
      end
    end
  end
end


# frozen_string_literal: true

require 'set'

module ROM
  module Plugins
    module Command
      # A plugin for automatically adding timestamp values
      # when executing a command
      #
      # Set up attributes to timestamp when the command is called
      #
      # @example
      #   class CreateTask < ROM::Commands::Create[:sql]
      #     result :one
      #     use :timestamps, timestamps: %i(created_at, updated_at), datestamps: %i(:written)
      #   end
      #
      #   create_user = rom.command(:user).create.curry(name: 'Jane')
      #
      #   result = create_user.call
      #   result[:created_at]  #=> Time.now.utc
      #
      # @api public
      class Timestamps < ::Module
        attr_reader :timestamps

        attr_reader :datestamps

        def initialize(timestamps: [], datestamps: [])
          super()
          @timestamps = store_attributes(timestamps)
          @datestamps = store_attributes(datestamps)
        end

        # @api private
        def store_attributes(attr)
          attr.is_a?(Array) ? attr : [attr]
        end

        # @api private
        def included(klass)
          initialize_timestamp_attributes(klass)
          klass.include(InstanceMethods)
          klass.extend(ClassInterface)
          super
        end

        def initialize_timestamp_attributes(klass)
          klass.defines :timestamp_columns, :datestamp_columns
          klass.timestamp_columns Set.new
          klass.datestamp_columns Set.new
          klass.before :set_timestamps
          klass.timestamp_columns klass.timestamp_columns.merge(timestamps) if timestamps.any?
          klass.datestamp_columns klass.datestamp_columns.merge(datestamps) if datestamps.any?
        end

        module InstanceMethods
          # @api private
          def timestamp_columns
            self.class.timestamp_columns
          end

          # @api private
          def datestamp_columns
            self.class.datestamp_columns
          end

          # Set the timestamp attributes on the given tuples
          #
          # @param [Array<Hash>, Hash] tuples the input tuple(s)
          #
          # @return [Array<Hash>, Hash]
          #
          # @api private
          def set_timestamps(tuples, *)
            timestamps = build_timestamps

            map_input_tuples(tuples) { |t| timestamps.merge(t) }
          end

          private

          # @api private
          def build_timestamps
            time        = Time.now.utc
            date        = Date.today
            timestamps  = {}

            timestamp_columns.each do |column|
              timestamps[column.to_sym] = time
            end

            datestamp_columns.each do |column|
              timestamps[column.to_sym] = date
            end

            timestamps
          end
        end

        module ClassInterface
          # @api private
          # Set up attributes to timestamp when the command is called
          #
          # @example
          #   class CreateTask < ROM::Commands::Create[:sql]
          #     result :one
          #     use :timestamps
          #     timestamps :created_at, :updated_at
          #   end
          #
          #   create_user = rom.command(:user).create.curry(name: 'Jane')
          #
          #   result = create_user.call
          #   result[:created_at]  #=> Time.now.utc
          #
          # @param [Array<Symbol>] names A list of attribute names
          #
          # @api public
          def timestamps(*names)
            timestamp_columns timestamp_columns.merge(names)
          end
          alias_method :timestamp, :timestamps

          # Set up attributes to datestamp when the command is called
          #
          # @example
          #   class CreateTask < ROM::Commands::Create[:sql]
          #     result :one
          #     use :timestamps
          #     datestamps :created_on, :updated_on
          #   end
          #
          #   create_user = rom.command(:user).create.curry(name: 'Jane')
          #
          #   result = create_user.call
          #   result[:created_at]  #=> Date.today
          #
          # @param [Array<Symbol>] names A list of attribute names
          #
          # @api public
          def datestamps(*names)
            datestamp_columns datestamp_columns.merge(names)
          end
          alias_method :datestamp, :datestamps
        end
      end
    end
  end
end


# frozen_string_literal: true

module ROM
  module Plugins
    module Command
      # Command plugin which sets input processing function via relation schema
      #
      # @api private
      module Schema
        def self.included(klass)
          super
          klass.extend(ClassInterface)
        end

        # @api private
        module ClassInterface
          # Build a command and set it input to relation's input_schema
          #
          # @see Command.build
          #
          # @return [Command]
          #
          # @api public
          def build(relation, **options)
            if relation.schema? && !options.key?(:input)
              relation_input = relation.input_schema
              command_input = input

              composed_input =
                if command_input.equal?(ROM::Command.input)
                  relation_input
                else
                  -> tuple { relation_input[command_input[tuple]] }
                end

              super(relation, **options, input: composed_input)
            else
              super
            end
          end
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'transproc/transformer'

require 'rom/changeset/pipe_registry'

module ROM
  class Changeset
    # Composable data transformation pipe used by default in changesets
    #
    # @api private
    class Pipe < Transproc::Transformer[PipeRegistry]
      extend Initializer

      define!(&:identity)

      param :processor, optional: true

      option :use_for_diff, optional: true, default: -> { true }
      option :diff_processor, default: -> { self[processor] }

      def self.new(*args, **opts)
        if args.empty?
          initialize(**opts)
        else
          super
        end
      end

      def self.initialize(**opts)
        transformer = allocate
        transformer.__send__(:initialize, dsl.(transformer), **opts)
        transformer
      end

      def self.[](name_or_proc)
        container[name_or_proc]
      end

      def [](*args)
        self.class[*args]
      end

      def bind(context)
        if processor.is_a?(Proc)
          bound_processor = self[-> *args { context.instance_exec(*args, &processor) }]
          bound_diff_processor = self[-> *args { context.instance_exec(*args, &diff_processor) }]

          new(bound_processor, diff_processor: bound_diff_processor)
        else
          self
        end
      end

      def compose(other, for_diff: other.is_a?(self.class) ? other.use_for_diff : false)
        new_proc = processor >> other

        if for_diff
          diff_proc = diff_processor >> (
            other.is_a?(self.class) ? other.diff_processor : other
          )

          new(new_proc, use_for_diff: true, diff_processor: diff_proc)
        else
          new(new_proc)
        end
      end
      alias_method :>>, :compose

      def call(data)
        processor.call(data)
      end

      def for_diff(data)
        use_for_diff ? diff_processor.call(data) : data
      end

      def new(processor, **opts)
        self.class.new(processor, **options, **opts)
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/changeset/pipe'

module ROM
  class Changeset
    # Stateful changesets carry data and can transform it into
    # a different structure compatible with a persistence backend
    #
    # @abstract
    class Stateful < Changeset
      # Default no-op pipe
      EMPTY_PIPE = Pipe.new(use_for_diff: false).freeze

      # @!attribute [r] __data__
      #   @return [Hash] The relation data
      #   @api private
      option :__data__, optional: true

      # @!attribute [r] pipe
      #   @return [Changeset::Pipe] data transformation pipe
      #   @api private
      option :pipe, reader: false, optional: true

      # Define a changeset mapping
      #
      # Subsequent mapping definitions will be composed together
      # and applied in the order they way defined
      #
      # @example Transformation DSL
      #   class NewUser < ROM::Changeset::Create
      #     map do
      #       unwrap :address, prefix: true
      #     end
      #   end
      #
      # @example Using custom block
      #   class NewUser < ROM::Changeset::Create
      #     map do |tuple|
      #       tuple.merge(created_at: Time.now)
      #     end
      #   end
      #
      # @example Multiple mappings (executed in the order of definition)
      #   class NewUser < ROM::Changeset::Create
      #     map do
      #       unwrap :address, prefix: true
      #     end
      #
      #     map do |tuple|
      #       tuple.merge(created_at: Time.now)
      #     end
      #   end
      #
      # @return [Array<Pipe>, Transproc::Function>]
      #
      # @see https://github.com/solnic/transproc Transproc
      #
      # @api public
      def self.map(**options, &block)
        if block.parameters.empty?
          pipes << Class.new(Pipe).define!(&block).new(**options)
        else
          pipes << Pipe.new(block, **options)
        end
      end

      # Define a changeset mapping excluded from diffs
      #
      # @see Changeset::Stateful.map
      # @see Changeset::Stateful#extend
      #
      # @return [Array<Pipe>, Transproc::Function>]
      #
      # @api public
      def self.extend(*, &)
        if block_given?
          map(use_for_diff: false, &)
        else
          super
        end
      end

      # Build default pipe object
      #
      # This can be overridden in a custom changeset subclass
      #
      # @return [Pipe]
      def self.default_pipe(context)
        pipes.empty? ? EMPTY_PIPE : pipes.map { |p| p.bind(context) }.reduce(:>>)
      end

      # @api private
      def self.inherited(klass)
        return if klass == ROM::Changeset

        super
        klass.instance_variable_set(:@__pipes__, pipes.dup)
      end

      # @api private
      def self.pipes
        @__pipes__
      end
      @__pipes__ = EMPTY_ARRAY

      # Pipe changeset's data using custom steps define on the pipe
      #
      # @overload map(*steps)
      #   Apply mapping using built-in transformations
      #
      #   @example
      #     changeset.map(:add_timestamps)
      #
      #   @param [Array<Symbol>] steps A list of mapping steps
      #
      # @overload map(&block)
      #   Apply mapping using a custom block
      #
      #   @example
      #     changeset.map { |tuple| tuple.merge(created_at: Time.now) }
      #
      # @overload map(*steps, &block)
      #   Apply mapping using built-in transformations and a custom block
      #
      #   @example
      #     changeset.map(:add_timestamps) { |tuple| tuple.merge(status: 'published') }
      #
      #   @param [Array<Symbol>] steps A list of mapping steps
      #
      # @return [Changeset]
      #
      # @api public
      def map(*steps, &)
        extend(*steps, for_diff: true, &)
      end

      # Pipe changeset's data using custom steps define on the pipe.
      # You should use #map instead except updating timestamp fields.
      # Calling changeset.extend builds a pipe that excludes certain
      # steps for generating the diff. Currently the only place where
      # it is used is update changesets with the `:touch` step, i.e.
      # `changeset.extend(:touch).diff` will exclude `:updated_at`
      # from the diff.
      #
      # @see Changeset::Stateful#map
      #
      # @return [Changeset]
      #
      # @api public
      def extend(*steps, **options, &)
        if block_given?
          if steps.empty?
            with(pipe: pipe.compose(Pipe.new(proc(&)).bind(self), **options))
          else
            extend(*steps, **options).extend(**options, &)
          end
        else
          with(pipe: steps.reduce(pipe.with(**options)) { |a, e| a.compose(pipe[e], **options) })
        end
      end

      # Return changeset with data
      #
      # @param [Hash] data
      #
      # @return [Changeset]
      #
      # @api public
      def data(data)
        with(__data__: data)
      end

      # Coerce changeset to a hash
      #
      # This will send the data through the pipe
      #
      # @return [Hash]
      #
      # @api public
      def to_h
        pipe.call(__data__)
      end
      alias_method :to_hash, :to_h

      # Coerce changeset to an array
      #
      # This will send the data through the pipe
      #
      # @return [Array]
      #
      # @api public
      def to_a
        result == :one ? [to_h] : __data__.map { |element| pipe.call(element) }
      end
      alias_method :to_ary, :to_a

      # Commit stateful changeset
      #
      # @see Changeset#commit
      #
      # @api public
      def commit
        command.call(self)
      end

      # Associate a changeset with another changeset or hash-like object
      #
      # @example with another changeset
      #   new_user = users.changeset(name: 'Jane')
      #   new_task = users.changeset(:tasks, title: 'A task')
      #
      #   new_task.associate(new_user, :users)
      #
      # @example with a hash-like object
      #   user = users.users.by_pk(1).one
      #   new_task = users.changeset(:tasks, title: 'A task')
      #
      #   new_task.associate(user, :users)
      #
      # @param [#to_hash, Changeset] other Other changeset or hash-like object
      # @param [Symbol] name The association identifier from schema
      #
      # @api public
      def associate(other, name = Associated.infer_assoc_name(other))
        Associated.new(self, associations: { name => other })
      end

      # Return command result type
      #
      # @return [Symbol]
      #
      # @api private
      def result
        __data__.is_a?(Array) ? :many : :one
      end

      # Return string representation of the changeset
      #
      # @return [String]
      #
      # @api public
      def inspect
        %(#<#{self.class} relation=#{relation.name.inspect} data=#{__data__}>)
      end

      # @api private
      def command_compiler_options
        super.merge(result: result)
      end

      # Data transformation pipe
      #
      # @return [Changeset::Pipe]
      #
      # @api private
      def pipe
        @pipe ||= self.class.default_pipe(self)
      end

      private

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

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

          if response.is_a?(__data__.class)
            with(__data__: response)
          else
            response
          end
        else
          super
        end
      end
    end
  end
end


def method_missing(meth, ...)
        if __data__.respond_to?(meth)
          response = __data__.__send__(meth, ...)

          if response.is_a?(__data__.class)
            with(__data__: response)
          else
            response
          end
        else
          super
        end
      end


# frozen_string_literal: true

module ROM
  class Changeset
    # Changeset specialization for update commands
    #
    # Update changesets will only execute their commands when
    # the data is different from the original tuple. Original tuple
    # is fetched from changeset's relation using `one` method.
    #
    # @example
    #   users.by_pk(1).changeset(:update, name: "Jane Doe").commit
    #
    # @see Changeset::Stateful
    #
    # @api public
    class Update < Stateful
      command_type :update

      # Commit update changeset if there's a diff
      #
      # This returns original tuple if there's no diff
      #
      # @return [Hash]
      #
      # @see Changeset#commit
      #
      # @api public
      def commit
        diff? ? super : original
      end

      # Return original tuple that this changeset may update
      #
      # @return [Hash]
      #
      # @api public
      def original
        @original ||= relation.one
      end

      # Return true if there's a diff between original and changeset data
      #
      # @return [TrueClass, FalseClass]
      #
      # @api public
      def diff?
        !diff.empty?
      end

      # Return if there's no diff between the original and changeset data
      #
      # @return [TrueClass, FalseClass]
      #
      # @api public
      def clean?
        diff.empty?
      end

      # Calculate the diff between the original and changeset data
      #
      # @return [Hash]
      #
      # @api public
      #
      # rubocop:disable Metrics/AbcSize
      def diff
        @diff ||=
          begin
            source = Hash(original)
            data = pipe.for_diff(__data__)
            data_tuple = data.to_a
            data_keys = data.keys & source.keys

            new_tuple = data_tuple.to_a.select { |k, _| data_keys.include?(k) }
            ori_tuple = source.to_a.select { |k, _| data_keys.include?(k) }

            (new_tuple - (new_tuple & ori_tuple)).to_h
          end
      end
      # rubocop:enable Metrics/AbcSize
    end
  end
end