# frozen_string_literal: true

module Shoryuken
  module Polling
    # A polling strategy that processes queues in weighted round-robin order.
    # Queue weights determine how often each queue is polled relative to others.
    # Queues are temporarily paused when no messages are found.
    class WeightedRoundRobin < BaseStrategy
      # Initializes a new WeightedRoundRobin polling strategy
      #
      # @param queues [Array<String>] array of queue names, with weights indicated by repetition
      # @param delay [Float, nil] delay in seconds before unpausing empty queues
      def initialize(queues, delay = nil)
        @initial_queues = queues
        @queues = queues.dup.uniq
        @paused_queues = []
        @delay = delay
      end

      # Returns the next queue to poll in round-robin order
      #
      # @return [QueueConfiguration, nil] the next queue configuration or nil if all paused
      def next_queue
        unpause_queues
        queue = @queues.shift
        return nil if queue.nil?

        @queues << queue
        QueueConfiguration.new(queue, {})
      end

      # Handles the result of polling a queue, adjusting weight if messages were found
      #
      # @param queue [String] the queue name
      # @param messages_found [Integer] number of messages found
      # @return [void]
      def messages_found(queue, messages_found)
        if messages_found == 0
          pause(queue)
          return
        end

        maximum_weight = maximum_queue_weight(queue)
        current_weight = current_queue_weight(queue)
        if maximum_weight > current_weight
          logger.info { "Increasing #{queue} weight to #{current_weight + 1}, max: #{maximum_weight}" }
          @queues << queue
        end
      end

      # Returns the list of active queues with their current weights
      #
      # @return [Array<Array>] array of [queue_name, weight] pairs
      def active_queues
        unparse_queues(@queues)
      end

      # Called when a message from a queue has been processed
      #
      # @param queue [String] the queue name
      # @return [void]
      def message_processed(queue)
        paused_queue = @paused_queues.find { |_time, name| name == queue }
        return unless paused_queue

        paused_queue[0] = Time.at 0
      end

      private

      # Pauses a queue by removing it from active rotation
      #
      # @param queue [String] the queue name to pause
      # @return [void]
      def pause(queue)
        return unless @queues.delete(queue)

        @paused_queues << [Time.now + delay, queue]
        logger.debug "Paused #{queue}"
      end

      # Unpauses queues whose delay has expired
      #
      # @return [void]
      def unpause_queues
        return if @paused_queues.empty?
        return if Time.now < @paused_queues.first[0]

        pause = @paused_queues.shift
        @queues << pause[1]
        logger.debug "Unpaused #{pause[1]}"
      end

      # Returns the current weight of a queue in the active rotation
      #
      # @param queue [String] the queue name
      # @return [Integer] the current weight
      def current_queue_weight(queue)
        queue_weight(@queues, queue)
      end

      # Returns the maximum configured weight of a queue
      #
      # @param queue [String] the queue name
      # @return [Integer] the maximum weight
      def maximum_queue_weight(queue)
        queue_weight(@initial_queues, queue)
      end

      # Counts how many times a queue appears in the given array
      #
      # @param queues [Array<String>] the array to count in
      # @param queue [String] the queue name to count
      # @return [Integer] the count
      def queue_weight(queues, queue)
        queues.count { |q| q == queue }
      end
    end
  end
end

pause

messages_found == 0
maximum_weight > current_weight
!(messages_found == 0)
!(maximum_weight > current_weight)

class WeightedRoundRobin < < BaseStrategy


# frozen_string_literal: true

module Shoryuken
  module Polling
    # A polling strategy that processes queues in strict priority order.
    # Higher priority queues are always processed before lower priority queues.
    # Queues are temporarily paused when no messages are found.
    class StrictPriority < BaseStrategy
      # Initializes a new StrictPriority polling strategy
      #
      # @param queues [Array<String>] array of queue names, with higher priority queues appearing more frequently
      # @param delay [Float, nil] delay in seconds before unpausing empty queues
      def initialize(queues, delay = nil)
        # Priority ordering of the queues, highest priority first
        @queues = queues
                  .group_by { |q| q }
                  .sort_by { |_, qs| -qs.count }
                  .map(&:first)

        # Pause status of the queues, default to past time (unpaused)
        @paused_until = queues
                        .each_with_object({}) { |queue, h| h[queue] = Time.at(0) }

        @delay = delay
        # Start queues at 0
        reset_next_queue
      end

      # Returns the next queue to poll based on strict priority
      #
      # @return [QueueConfiguration, nil] the next queue configuration or nil if all paused
      def next_queue
        next_queue = next_active_queue
        next_queue.nil? ? nil : QueueConfiguration.new(next_queue, {})
      end

      # Handles the result of polling a queue
      #
      # @param queue [String] the queue name
      # @param messages_found [Integer] number of messages found
      # @return [void]
      def messages_found(queue, messages_found)
        if messages_found == 0
          pause(queue)
        else
          reset_next_queue
        end
      end

      # Returns the list of active (non-paused) queues with their priorities
      #
      # @return [Array<Array>] array of [queue_name, priority] pairs
      def active_queues
        @queues
          .reverse
          .map.with_index(1)
          .reject { |q, _| queue_paused?(q) }
          .reverse
      end

      # Called when a message from a queue has been processed
      #
      # @param queue [String] the queue name
      # @return [void]
      def message_processed(queue)
        if queue_paused?(queue)
          logger.debug "Unpausing #{queue}"
          @paused_until[queue] = Time.at 0
        end
      end

      private

      # Finds the next active (non-paused) queue
      #
      # @return [String, nil] the queue name or nil if all paused
      def next_active_queue
        reset_next_queue if queues_unpaused_since?

        size = @queues.length
        size.times do
          queue = @queues[@next_queue_index]
          @next_queue_index = (@next_queue_index + 1) % size
          return queue unless queue_paused?(queue)
        end

        nil
      end

      # Checks if any queues have been unpaused since last check
      #
      # @return [Boolean] true if queues were unpaused
      def queues_unpaused_since?
        last = @last_unpause_check
        now = @last_unpause_check = Time.now

        last && @paused_until.values.any? { |t| t > last && t <= now }
      end

      # Resets the next queue index to start from the highest priority
      #
      # @return [void]
      def reset_next_queue
        @next_queue_index = 0
      end

      # Checks if a queue is currently paused
      #
      # @param queue [String] the queue name
      # @return [Boolean] true if the queue is paused
      def queue_paused?(queue)
        @paused_until[queue] > Time.now
      end

      # Pauses a queue for the configured delay time
      #
      # @param queue [String] the queue name to pause
      # @return [void]
      def pause(queue)
        return unless delay > 0

        @paused_until[queue] = Time.now + delay
        logger.debug "Paused #{queue}"
      end
    end
  end
end


def next_active_queue
        reset_next_queue if queues_unpaused_since?

        size = @queues.length
        size.times do
          queue = @queues[@next_queue_index]
          @next_queue_index = (@next_queue_index + 1) % size
          return queue unless queue_paused?(queue)
        end

        nil
      end

      def queue_paused?(queue)
        @paused_until[queue] > Time.now
      end


# frozen_string_literal: true

module Shoryuken
  module Worker
    # Default executor that sends messages to SQS for asynchronous processing.
    # This is the standard executor used in production environments.
    class DefaultExecutor
      class << self
        # Enqueues a job for asynchronous processing via SQS
        #
        # @param worker_class [Class] the worker class that will process the message
        # @param body [Object] the message body
        # @param options [Hash] additional SQS message options
        # @option options [Hash] :message_attributes custom message attributes
        # @option options [String] :queue override the default queue
        # @return [Aws::SQS::Types::SendMessageResult] the send result
        def perform_async(worker_class, body, options = {})
          options[:message_attributes] ||= {}
          options[:message_attributes]['shoryuken_class'] = {
            string_value: worker_class.to_s,
            data_type: 'String'
          }

          options[:message_body] = body

          queue = options.delete(:queue) || worker_class.get_shoryuken_options['queue']

          Shoryuken::Client.queues(queue).send_message(options)
        end

        # Enqueues a job for delayed processing via SQS
        #
        # @param worker_class [Class] the worker class that will process the message
        # @param interval [Integer, Float] delay in seconds or timestamp
        # @param body [Object] the message body
        # @param options [Hash] SQS message options for the delayed job
        # @option options [Hash] :message_attributes custom message attributes
        # @option options [String] :queue override the default queue
        # @return [Aws::SQS::Types::SendMessageResult] the send result
        # @raise [Errors::InvalidDelayError] if delay exceeds 15 minutes
        def perform_in(worker_class, interval, body, options = {})
          interval = interval.to_f
          now = Time.now.to_f
          ts = (interval < 1_000_000_000 ? (now + interval).to_f : interval)

          delay = (ts - now).ceil

          raise Errors::InvalidDelayError, 'The maximum allowed delay is 15 minutes' if delay > 15 * 60

          worker_class.perform_async(body, options.merge(delay_seconds: delay))
        end
      end
    end
  end
end

queues

      def queues(name)
        @@queues[name.to_s] ||= Shoryuken::Queue.new(sqs, name)
      end

    def send_message(options)
      options = sanitize_message!(options).merge(queue_url: url)

      Shoryuken.client_middleware.invoke(options) do
        client.send_message(options)
      end
    end


# frozen_string_literal: true

module Shoryuken
  module Worker
    # Executor that processes jobs synchronously in the current thread.
    # Useful for testing and development environments.
    class InlineExecutor
      class << self
        # Processes a job synchronously in the current thread
        #
        # @param worker_class [Class] the worker class that will process the message
        # @param body [Object] the message body
        # @param options [Hash] inline execution options
        # @option options [String] :queue override the default queue name
        # @option options [Hash] :message_attributes custom message attributes
        # @return [Object] the result of the worker's perform method
        def perform_async(worker_class, body, options = {})
          body = JSON.dump(body) if body.is_a?(Hash)
          queue_name = options.delete(:queue) || worker_class.get_shoryuken_options['queue']
          message_attributes = options.delete(:message_attributes) || {}
          message_attributes['shoryuken_class'] = {
            string_value: worker_class.to_s,
            data_type: 'String'
          }

          sqs_msg = InlineMessage.new(
            body: body,
            attributes: nil,
            md5_of_body: nil,
            md5_of_message_attributes: nil,
            message_attributes: message_attributes,
            message_id: nil,
            receipt_handle: nil,
            delete: nil,
            queue_name: queue_name
          )

          call(worker_class, sqs_msg)
        end

        # Processes a job synchronously, ignoring the delay interval
        #
        # @param worker_class [Class] the worker class that will process the message
        # @param _interval [Integer, Float] ignored for inline execution
        # @param body [Object] the message body
        # @param options [Hash] inline execution options
        # @option options [String] :queue override the default queue name
        # @option options [Hash] :message_attributes custom message attributes
        # @return [Object] the result of the worker's perform method
        def perform_in(worker_class, _interval, body, options = {})
          worker_class.perform_async(body, options)
        end

        private

        # Instantiates and calls the worker
        #
        # @param worker_class [Class] the worker class
        # @param sqs_msg [Shoryuken::InlineMessage] the message
        # @return [Object] the result of the worker's perform method
        def call(worker_class, sqs_msg)
          parsed_body = BodyParser.parse(worker_class, sqs_msg)
          batch = worker_class.shoryuken_options_hash['batch']
          args = batch ? [[sqs_msg], [parsed_body]] : [sqs_msg, parsed_body]
          worker_class.new.perform(*args)
        end
      end
    end
  end
end


# frozen_string_literal: true

module Shoryuken
  # Middleware provides a way to wrap message processing with custom logic,
  # similar to Rack middleware in web applications. Middleware runs on the server
  # side and can perform setup, teardown, error handling, and monitoring around
  # job execution.
  #
  # Middleware classes must implement a `call` method that accepts the worker instance,
  # queue name, and SQS message, and must yield to continue the middleware chain.
  #
  # ## Global Middleware Configuration
  #
  # Configure middleware globally for all workers:
  #
  #   Shoryuken.configure_server do |config|
  #     config.server_middleware do |chain|
  #       chain.add MyServerHook
  #       chain.remove Shoryuken::Middleware::Server::ActiveRecord
  #     end
  #   end
  #
  # ## Per-Worker Middleware Configuration
  #
  # Configure middleware for specific workers:
  #
  #   class MyWorker
  #     include Shoryuken::Worker
  #
  #     server_middleware do |chain|
  #       chain.add MyWorkerSpecificMiddleware
  #     end
  #   end
  #
  # ## Middleware Ordering
  #
  # Insert middleware at specific positions in the chain:
  #
  #   # Insert before existing middleware
  #   chain.insert_before Shoryuken::Middleware::Server::ActiveRecord, MyDatabaseSetup
  #
  #   # Insert after existing middleware
  #   chain.insert_after Shoryuken::Middleware::Server::Timing, MyMetricsCollector
  #
  #   # Add to beginning of chain
  #   chain.prepend MyFirstMiddleware
  #
  # ## Example Middleware Implementations
  #
  #   # Basic logging middleware
  #   class LoggingMiddleware
  #     def call(worker_instance, queue, sqs_msg, body)
  #       puts "Processing #{sqs_msg.message_id} on #{queue}"
  #       start_time = Time.now
  #       yield
  #       puts "Completed in #{Time.now - start_time}s"
  #     end
  #   end
  #
  #   # Error reporting middleware
  #   class ErrorReportingMiddleware
  #     def call(worker_instance, queue, sqs_msg, body)
  #       yield
  #     rescue => error
  #       ErrorReporter.notify(error, {
  #         worker: worker_instance.class.name,
  #         queue: queue,
  #         message_id: sqs_msg.message_id
  #       })
  #       raise
  #     end
  #   end
  #
  #   # Performance monitoring middleware
  #   class MetricsMiddleware
  #     def call(worker_instance, queue, sqs_msg, body)
  #       start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  #       yield
  #       duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
  #       StatsD.timing("shoryuken.#{worker_instance.class.name.underscore}.duration", duration)
  #     end
  #   end
  #
  # @see Shoryuken::Middleware::Chain Middleware chain management
  # @see https://github.com/ruby-shoryuken/shoryuken/wiki/Middleware Comprehensive middleware guide
  module Middleware
    # Manages a chain of middleware classes that will be instantiated and invoked
    # in sequence around message processing. Provides methods for adding, removing,
    # and reordering middleware.
    class Chain
      # @return [Array<Entry>] The ordered list of middleware entries
      attr_reader :entries

      # Creates a new middleware chain.
      #
      # @yield [Chain] The chain instance for configuration
      # @example Creating and configuring a chain
      #   chain = Shoryuken::Middleware::Chain.new do |c|
      #     c.add MyMiddleware
      #     c.add AnotherMiddleware, option: 'value'
      #   end
      def initialize
        @entries = []
        yield self if block_given?
      end

      # Creates a copy of this middleware chain.
      #
      # @return [Chain] A new chain with the same middleware entries
      def dup
        self.class.new.tap { |new_chain| new_chain.entries.replace(entries) }
      end

      # Removes all instances of the specified middleware class from the chain.
      #
      # @param klass [Class] The middleware class to remove
      # @return [Array<Entry>] The removed entries
      # @example Removing ActiveRecord middleware
      #   chain.remove Shoryuken::Middleware::Server::ActiveRecord
      def remove(klass)
        entries.delete_if { |entry| entry.klass == klass }
      end

      # Adds middleware to the end of the chain. Does nothing if the middleware
      # class is already present in the chain.
      #
      # @param klass [Class] The middleware class to add
      # @param args [Array] Arguments to pass to the middleware constructor
      # @example Adding middleware with arguments
      #   chain.add MyMiddleware, timeout: 30, retries: 3
      def add(klass, *args)
        entries << Entry.new(klass, *args) unless exists?(klass)
      end

      # Adds middleware to the beginning of the chain. Does nothing if the middleware
      # class is already present in the chain.
      #
      # @param klass [Class] The middleware class to prepend
      # @param args [Array] Arguments to pass to the middleware constructor
      # @example Adding middleware to run first
      #   chain.prepend AuthenticationMiddleware
      def prepend(klass, *args)
        entries.insert(0, Entry.new(klass, *args)) unless exists?(klass)
      end

      # Inserts middleware immediately before another middleware class.
      # If the new middleware already exists, it's moved to the new position.
      #
      # @param oldklass [Class] The existing middleware to insert before
      # @param newklass [Class] The middleware class to insert
      # @param args [Array] Arguments to pass to the middleware constructor
      # @example Insert database setup before ActiveRecord middleware
      #   chain.insert_before Shoryuken::Middleware::Server::ActiveRecord, DatabaseSetup
      def insert_before(oldklass, newklass, *args)
        i = entries.index { |entry| entry.klass == newklass }
        new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
        i = entries.find_index { |entry| entry.klass == oldklass } || 0
        entries.insert(i, new_entry)
      end

      # Inserts middleware immediately after another middleware class.
      # If the new middleware already exists, it's moved to the new position.
      #
      # @param oldklass [Class] The existing middleware to insert after
      # @param newklass [Class] The middleware class to insert
      # @param args [Array] Arguments to pass to the middleware constructor
      # @example Insert metrics collection after timing middleware
      #   chain.insert_after Shoryuken::Middleware::Server::Timing, MetricsCollector
      def insert_after(oldklass, newklass, *args)
        i = entries.index { |entry| entry.klass == newklass }
        new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
        i = entries.find_index { |entry| entry.klass == oldklass } || entries.count - 1
        entries.insert(i + 1, new_entry)
      end

      # Checks if a middleware class is already in the chain.
      #
      # @param klass [Class] The middleware class to check for
      # @return [Boolean] True if the middleware is in the chain
      def exists?(klass)
        entries.any? { |entry| entry.klass == klass }
      end

      # Creates instances of all middleware classes in the chain.
      #
      # @return [Array] Array of middleware instances
      def retrieve
        entries.map(&:make_new)
      end

      # Removes all middleware from the chain.
      #
      # @return [Array] Empty array
      def clear
        entries.clear
      end

      # Invokes the middleware chain with the given arguments.
      # Each middleware's call method will be invoked in sequence,
      # with control passed through yielding.
      #
      # @param args [Array] arguments to pass to each middleware
      # @param final_action [Proc] the final action to perform after all middleware
      # @return [void]
      def invoke(*args, &final_action)
        chain = retrieve.dup
        traverse_chain = lambda do
          if chain.empty?
            final_action.call
          else
            chain.shift.call(*args, &traverse_chain)
          end
        end
        traverse_chain.call
      end
    end
  end
end


# frozen_string_literal: true

require 'active_support/current_attributes'
require 'active_job'

module Shoryuken
  # ActiveJob integration module for Shoryuken
  module ActiveJob
    # Middleware to persist Rails CurrentAttributes across job execution.
    #
    # This ensures that request-scoped context (like current user, tenant, locale)
    # automatically flows from the code that enqueues a job to the job's execution.
    #
    # Based on Sidekiq's approach to persisting current attributes.
    #
    # @example Setup in initializer
    #   require 'shoryuken/active_job/current_attributes'
    #   Shoryuken::ActiveJob::CurrentAttributes.persist('MyApp::Current')
    #
    # @example Multiple CurrentAttributes classes
    #   Shoryuken::ActiveJob::CurrentAttributes.persist('MyApp::Current', 'MyApp::RequestContext')
    #
    # @see https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
    # @see https://github.com/sidekiq/sidekiq/blob/main/lib/sidekiq/middleware/current_attributes.rb
    module CurrentAttributes
      # Serializer for current attributes using ActiveJob::Arguments.
      # Supports Symbols and GlobalID objects.
      module Serializer
        module_function

        # Serializes attributes hash for SQS message storage
        #
        # @param attrs [Hash] the attributes to serialize
        # @return [Object] the serialized attributes
        def serialize(attrs)
          ::ActiveJob::Arguments.serialize([attrs]).first
        end

        # Deserializes attributes hash from SQS message
        #
        # @param attrs [Object] the serialized attributes
        # @return [Hash] the deserialized attributes
        def deserialize(attrs)
          ::ActiveJob::Arguments.deserialize([attrs]).first
        end
      end

      class << self
        # @return [Hash{String => String}] serialization keys mapped to CurrentAttributes class names
        attr_reader :cattrs

        # Register CurrentAttributes classes to persist across job execution.
        #
        # @param klasses [Array<String, Class>] CurrentAttributes class names or classes
        # @example
        #   Shoryuken::ActiveJob::CurrentAttributes.persist('Current')
        #   Shoryuken::ActiveJob::CurrentAttributes.persist(Current, RequestContext)
        def persist(*klasses)
          @cattrs ||= {}

          klasses.flatten.each_with_index do |klass, idx|
            key = @cattrs.empty? ? 'cattr' : "cattr_#{idx}"
            @cattrs[key] = klass.to_s
          end

          # Prepend the persistence module to the adapter for serialization
          unless ::ActiveJob::QueueAdapters::ShoryukenAdapter.ancestors.include?(Persistence)
            ::ActiveJob::QueueAdapters::ShoryukenAdapter.prepend(Persistence)
          end

          # Prepend the loading module to JobWrapper for deserialization
          unless Shoryuken::ActiveJob::JobWrapper.ancestors.include?(Loading)
            Shoryuken::ActiveJob::JobWrapper.prepend(Loading)
          end
        end
      end

      # Module prepended to ShoryukenAdapter to serialize CurrentAttributes on enqueue.
      module Persistence
        private

        # Builds the SQS message with CurrentAttributes data
        #
        # @param queue [Shoryuken::Queue] the target queue
        # @param job [ActiveJob::Base] the job being enqueued
        # @return [Hash] the message parameters
        def message(queue, job)
          hash = super

          CurrentAttributes.cattrs&.each do |key, klass_name|
            next if hash[:message_body].key?(key)

            klass = klass_name.constantize
            attrs = klass.attributes
            next if attrs.empty?

            hash[:message_body][key] = Serializer.serialize(attrs)
          end

          hash
        end
      end

      # Module prepended to JobWrapper to restore CurrentAttributes on execute.
      module Loading
        # Performs the job after restoring CurrentAttributes
        #
        # @param sqs_msg [Shoryuken::Message] the SQS message
        # @param hash [Hash] the deserialized job data
        # @return [void]
        def perform(sqs_msg, hash)
          klasses_to_reset = []

          CurrentAttributes.cattrs&.each do |key, klass_name|
            next unless hash.key?(key)

            klass = klass_name.constantize
            klasses_to_reset << klass

            begin
              attrs = Serializer.deserialize(hash[key])
              attrs.each do |attr_name, value|
                klass.public_send(:"#{attr_name}=", value) if klass.respond_to?(:"#{attr_name}=")
              end
            rescue => e
              # Log but don't fail if attributes can't be restored
              # (e.g., attribute removed between enqueue and execute)
              Shoryuken.logger.warn("Failed to restore CurrentAttributes #{klass_name}: #{e.message}")
            end
          end

          super
        ensure
          klasses_to_reset.each(&:reset)
        end
      end
    end
  end
end


def message(queue, job)
          hash = super

          CurrentAttributes.cattrs&.each do |key, klass_name|
            next if hash[:message_body].key?(key)

            klass = klass_name.constantize
            attrs = klass.attributes
            next if attrs.empty?

            hash[:message_body][key] = Serializer.serialize(attrs)
          end

          hash
        end

message

serialize

        def serialize(attrs)
          ::ActiveJob::Arguments.serialize([attrs]).first
        end


def perform(sqs_msg, hash)
          klasses_to_reset = []

          CurrentAttributes.cattrs&.each do |key, klass_name|
            next unless hash.key?(key)

            klass = klass_name.constantize
            klasses_to_reset << klass

            begin
              attrs = Serializer.deserialize(hash[key])
              attrs.each do |attr_name, value|
                klass.public_send(:"#{attr_name}=", value) if klass.respond_to?(:"#{attr_name}=")
              end
            rescue => e
              # Log but don't fail if attributes can't be restored
              # (e.g., attribute removed between enqueue and execute)
              Shoryuken.logger.warn("Failed to restore CurrentAttributes #{klass_name}: #{e.message}")
            end
          end

          super
        ensure
          klasses_to_reset.each(&:reset)
        end

perform

deserialize

        def deserialize(attrs)
          ::ActiveJob::Arguments.deserialize([attrs]).first
        end


# frozen_string_literal: true

module Shoryuken
  module Helpers
    # A thread-safe timer task implementation.
    # Drop-in replacement for Concurrent::TimerTask without external dependencies.
    class TimerTask
      # Initializes a new TimerTask
      #
      # @param execution_interval [Float] interval in seconds between task executions
      # @param task [Proc] the task to execute on each interval (provided as a block)
      # @return [TimerTask] a new TimerTask instance
      # @raise [ArgumentError] if no block is provided or interval is not positive
      # @yield the task to execute on each interval
      def initialize(execution_interval:, &task)
        raise ArgumentError, 'A block must be provided' unless block_given?

        @execution_interval = Float(execution_interval)
        raise ArgumentError, 'execution_interval must be positive' if @execution_interval <= 0

        @task = task
        @mutex = Mutex.new
        @thread = nil
        @running = false
        @killed = false
      end

      # Starts the timer task execution
      #
      # @return [TimerTask] self for method chaining
      def execute
        @mutex.synchronize do
          return self if @running || @killed

          @running = true
          @thread = Thread.new { run_timer_loop }
        end
        self
      end

      # Stops and kills the timer task
      #
      # @return [Boolean] true if killed, false if already killed
      def kill
        @mutex.synchronize do
          return false if @killed

          @killed = true
          @running = false

          @thread.kill if @thread&.alive?
        end
        true
      end

      private

      # Runs the timer loop in a separate thread
      #
      # @return [void]
      def run_timer_loop
        until @killed
          sleep(@execution_interval)
          break if @killed

          begin
            @task.call
          rescue => e
            # Log the error but continue running
            # This matches the behavior of Concurrent::TimerTask
            warn "TimerTask execution error: #{e.message}"
            warn e.backtrace.join("\n") if e.backtrace
          end
        end
      ensure
        @mutex.synchronize { @running = false }
      end
    end
  end
end


def run_timer_loop
        until @killed
          sleep(@execution_interval)
          break if @killed

          begin
            @task.call
          rescue => e
            # Log the error but continue running
            # This matches the behavior of Concurrent::TimerTask
            warn "TimerTask execution error: #{e.message}"
            warn e.backtrace.join("\n") if e.backtrace
          end
        end
      ensure
        @mutex.synchronize { @running = false }
      end

join

        # @return [void]
        def print_table(entries)
          column_sizes = print_columns_size(entries)

          entries.map do |entry|
            puts entry.map.with_index { |e, i| print_format_column(e, column_sizes[i]) }.join
          end
        end

        # Calculates the maximum width for each column
        #

    # @param _queue [String] the queue name where the error occurred (unused)
    # @param _sqs_msg [Shoryuken::Message] the message being processed (unused)
    # @return [void]
    def self.call(exception, _queue, _sqs_msg)
      logger.error { "Processor failed: #{exception.message}" }
      logger.error { exception.backtrace.join("\n") } if exception.backtrace
    end
  end
end

synchronize

      # Sets the value to true
      #
      # @return [Boolean] true
      def make_true
        @mutex.synchronize { @value = 1 }
        true
      end

      # Sets the value to false
      #

      # Sets the value to false
      #
      # @return [Boolean] false
      def make_false
        @mutex.synchronize { @value = 0 }
        false
      end

      # Checks if the value is true
      #


# frozen_string_literal: true

module Shoryuken
  module Middleware
    module Server
      # Middleware that logs timing information for message processing.
      # Records start time, completion time, and warns if processing
      # exceeds the queue's visibility timeout.
      class Timing
        include Util

        # Processes a message while logging timing information
        #
        # @param _worker [Object] the worker instance (unused)
        # @param queue [String] the queue name
        # @param _sqs_msg [Shoryuken::Message] the message being processed (unused)
        # @param _body [Object] the parsed message body (unused)
        # @yield continues to the next middleware in the chain
        # @return [void]
        def call(_worker, queue, _sqs_msg, _body)
          started_at = Time.now

          logger.info { "started at #{started_at}" }

          yield

          total_time = elapsed(started_at)

          if (total_time / 1000.0) > (timeout = Shoryuken::Client.queues(queue).visibility_timeout)
            logger.warn { "exceeded the queue visibility timeout by #{total_time - (timeout * 1000)} ms" }
          end

          logger.info { "completed in: #{total_time} ms" }
        rescue
          logger.info { "failed in: #{elapsed(started_at)} ms" }
          raise
        end
      end
    end
  end
end

call

elapsed

    def elapsed(started_at)
      # elapsed in ms
      (Time.now - started_at) * 1000
    end

queues

!(total_time / 1000.0) > (timeout = Shoryuken::Client.queues(queue).visibility_timeout)

    def visibility_timeout
      # Always lookup for the latest visibility when cache is disabled
      # setting it to nil, forces re-lookup
      @_visibility_timeout = nil unless Shoryuken.cache_visibility_timeout?
      @_visibility_timeout ||= queue_attributes.attributes[VISIBILITY_TIMEOUT_ATTR].to_i
    end


# frozen_string_literal: true

module Shoryuken
  module Middleware
    module Server
      # Middleware that handles non-retryable exceptions by deleting messages immediately.
      # When a configured exception occurs, the message is deleted instead of being retried.
      #
      # Configure non-retryable exceptions per worker:
      #
      #   class MyWorker
      #     include Shoryuken::Worker
      #
      #     # Using exception classes
      #     shoryuken_options queue: 'my_queue',
      #                       non_retryable_exceptions: [InvalidInputError, RecordNotFoundError]
      #
      #     # Or using a lambda for dynamic classification
      #     shoryuken_options queue: 'my_queue',
      #                       non_retryable_exceptions: ->(error) {
      #                         error.is_a?(StandardError) && error.message.include?('permanent')
      #                       }
      #
      #     def perform(sqs_msg, body)
      #       # ...
      #     end
      #   end
      class NonRetryableException
        include Util

        # Processes a message and handles non-retryable exceptions
        #
        # @param worker [Object] the worker instance
        # @param queue [String] the queue name
        # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
        # @param _body [Object] the parsed message body (unused)
        # @yield continues to the next middleware in the chain
        # @return [void]
        def call(worker, queue, sqs_msg, _body)
          yield
        rescue => e
          non_retryable_exceptions = worker.class.get_shoryuken_options['non_retryable_exceptions']

          return raise unless non_retryable_exceptions

          if non_retryable_exceptions.respond_to?(:call)
            return raise unless non_retryable_exceptions.call(e)
          else
            exception_classes = Array(non_retryable_exceptions)
            return raise unless exception_classes.any? { |klass| e.is_a?(klass) }
          end

          # Handle batch messages
          messages = sqs_msg.is_a?(Array) ? sqs_msg : [sqs_msg]

          logger.warn do
            "Non-retryable exception #{e.class} occurred for message(s) #{messages.map(&:message_id).join(', ')}. " \
            "Deleting message(s) immediately. Error: #{e.message}"
          end

          logger.debug { e.backtrace.join("\n") } if e.backtrace

          # Delete the message(s) immediately
          entries = messages.map.with_index { |message, i| { id: i.to_s, receipt_handle: message.receipt_handle } }

          begin
            queue_client = Shoryuken::Client.queues(queue)
            delete_failed = queue_client.delete_messages(entries: entries)

            # Check if deletion reported failures (returns true if any failed)
            if delete_failed
              logger.warn do
                'Failed to delete some messages for non-retryable exception on queue ' \
                  "'#{queue}'. " \
                  "Entries: #{entries.map { |e| { id: e[:id] } }.inspect}. " \
                  'Some messages may remain in the queue and could be reprocessed.'
              end
            end
          rescue => delete_error
            logger.error do
              'Error deleting messages for non-retryable exception on queue ' \
                "'#{queue}': #{delete_error.class} - #{delete_error.message}. " \
                "Entries: #{entries.map { |e| { id: e[:id] } }.inspect}. " \
                'Messages may remain in the queue and could be reprocessed.'
            end
            logger.debug { delete_error.backtrace.join("\n") } if delete_error.backtrace
          end

          # Don't re-raise - the exception has been handled by deleting the message
        end
      end
    end
  end
end

call

    def delete_messages(options)
      failed_messages = client.delete_message_batch(
        options.merge(queue_url: url)
      ).failed || []
      failed_messages.any? do |failure|
        logger.error do
          "Could not delete #{failure.id}, code: '#{failure.code}', message: '#{failure.message}', sender_fault: #{failure.sender_fault}"
        end
      end
    end


# frozen_string_literal: true

module Shoryuken
  module Middleware
    module Server
      # Middleware that automatically extends message visibility timeout during processing.
      # Prevents messages from becoming visible to other consumers while still being processed.
      class AutoExtendVisibility
        include Util

        # Number of seconds before timeout to extend visibility
        EXTEND_UPFRONT_SECONDS = 5

        # Processes a message with automatic visibility timeout extension
        #
        # @param worker [Object] the worker instance
        # @param queue [String] the queue name
        # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
        # @param body [Object] the parsed message body
        # @yield continues to the next middleware in the chain
        # @return [void]
        def call(worker, queue, sqs_msg, body)
          return yield unless worker.class.auto_visibility_timeout?

          if sqs_msg.is_a?(Array)
            logger.warn { "Auto extend visibility isn't supported for batch workers" }
            return yield
          end

          timer = auto_visibility_timer(worker, queue, sqs_msg, body)
          yield
        ensure
          timer.kill if timer
        end

        private

        # Helper class for extending message visibility
        class MessageVisibilityExtender
          include Util

          # Creates a timer task that extends message visibility
          #
          # @param _worker [Object] the worker instance (unused)
          # @param queue [String] the queue name
          # @param sqs_msg [Shoryuken::Message] the message
          # @param _body [Object] the parsed message body (unused)
          # @return [Shoryuken::Helpers::TimerTask] the timer task
          def auto_extend(_worker, queue, sqs_msg, _body)
            queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout

            Shoryuken::Helpers::TimerTask.new(execution_interval: queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
              logger.debug do
                "Extending message #{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s"
              end

              sqs_msg.change_visibility(visibility_timeout: queue_visibility_timeout)
            rescue => e
              logger.error do
                "Could not auto extend the message #{queue}/#{sqs_msg.message_id} visibility timeout. Error: #{e.message}"
              end
            end
          end
        end

        # Creates and starts a visibility extension timer
        #
        # @param worker [Object] the worker instance
        # @param queue [String] the queue name
        # @param sqs_msg [Shoryuken::Message] the message
        # @param body [Object] the parsed message body
        # @return [Shoryuken::Helpers::TimerTask] the started timer
        def auto_visibility_timer(worker, queue, sqs_msg, body)
          MessageVisibilityExtender.new.auto_extend(worker, queue, sqs_msg, body).tap(&:execute)
        end
      end
    end
  end
end


def auto_extend(_worker, queue, sqs_msg, _body)
            queue_visibility_timeout = Shoryuken::Client.queues(queue).visibility_timeout

            Shoryuken::Helpers::TimerTask.new(execution_interval: queue_visibility_timeout - EXTEND_UPFRONT_SECONDS) do
              logger.debug do
                "Extending message #{queue}/#{sqs_msg.message_id} visibility timeout by #{queue_visibility_timeout}s"
              end

              sqs_msg.change_visibility(visibility_timeout: queue_visibility_timeout)
            rescue => e
              logger.error do
                "Could not auto extend the message #{queue}/#{sqs_msg.message_id} visibility timeout. Error: #{e.message}"
              end
            end
          end

    def change_visibility(options)
      client.change_message_visibility(
        options.merge(queue_url: queue_url, receipt_handle: data.receipt_handle)
      )
    end


# frozen_string_literal: true

module Shoryuken
  module Middleware
    module Server
      # Middleware that implements exponential backoff retry for failed messages.
      # When a job fails, the message visibility timeout is adjusted based on
      # configured retry intervals.
      class ExponentialBackoffRetry
        include Util

        # Processes a message with exponential backoff retry on failure
        #
        # @param worker [Object] the worker instance
        # @param _queue [String] the queue name (unused)
        # @param sqs_msg [Shoryuken::Message, Array<Shoryuken::Message>] the message or batch
        # @param _body [Object] the parsed message body (unused)
        # @yield continues to the next middleware in the chain
        # @return [void]
        # @raise [StandardError] re-raises the original exception if retry intervals are not configured
        #   or if retry limit is exceeded
        def call(worker, _queue, sqs_msg, _body)
          return yield unless worker.class.exponential_backoff?

          if sqs_msg.is_a?(Array)
            logger.warn { "Exponential backoff isn't supported for batch workers" }
            return yield
          end

          started_at = Time.now
          yield
        rescue => e
          retry_intervals = worker.class.get_shoryuken_options['retry_intervals']

          if retry_intervals.nil? || !handle_failure(sqs_msg, started_at, retry_intervals)
            # Re-raise the exception if the job is not going to be exponential backoff retried.
            # This allows custom middleware (like exception notifiers) to be aware of the unhandled failure.
            raise
          end

          logger.warn { "Message #{sqs_msg.message_id} will attempt retry due to error: #{e.message}" }
          # since we didn't raise, lets log the backtrace for debugging purposes.
          logger.debug { e.backtrace.join("\n") } unless e.backtrace.nil?
        end

        private

        # Gets the retry interval for a given attempt number
        #
        # @param retry_intervals [Array<Integer>, #call] the configured intervals or callable
        # @param attempts [Integer] the current attempt number
        # @return [Integer, nil] the interval in seconds or nil
        def get_interval(retry_intervals, attempts)
          return retry_intervals.call(attempts) if retry_intervals.respond_to?(:call)

          if attempts <= (retry_intervals = Array(retry_intervals)).size
            retry_intervals[attempts - 1]
          else
            retry_intervals.last
          end
        end

        # Calculates the next visibility timeout capped at SQS maximum
        #
        # @param interval [Integer] the desired interval
        # @param started_at [Time] when processing started
        # @return [Integer] the capped visibility timeout
        def next_visibility_timeout(interval, started_at)
          max_timeout = 43_200 - (Time.now - started_at).ceil - 1
          interval = max_timeout if interval > max_timeout
          interval.to_i
        end

        # Handles a message failure by adjusting visibility timeout
        #
        # @param sqs_msg [Shoryuken::Message] the failed message
        # @param started_at [Time] when processing started
        # @param retry_intervals [Array<Integer>, #call] the configured intervals
        # @return [Boolean] true if retry was scheduled, false otherwise
        def handle_failure(sqs_msg, started_at, retry_intervals)
          receive_count = sqs_msg.attributes['ApproximateReceiveCount'].to_i

          return false unless (interval = get_interval(retry_intervals, receive_count))

          sqs_msg.change_visibility(visibility_timeout: next_visibility_timeout(interval.to_i, started_at))

          logger.info { "Message #{sqs_msg.message_id} failed, will be retried in #{interval} seconds" }

          true
        end
      end
    end
  end
end


# frozen_string_literal: true

# ActiveJob docs: http://edgeguides.rubyonrails.org/active_job_basics.html
# Example adapters ref: https://github.com/rails/rails/tree/master/activejob/lib/active_job/queue_adapters

require 'shoryuken'
require 'shoryuken/active_job/job_wrapper'

# Rails ActiveJob module providing background job processing
module ActiveJob
  # Queue adapter implementations for various backends
  module QueueAdapters
    # Shoryuken adapter for Active Job.
    # To use Shoryuken set the queue_adapter config to +:shoryuken+.
    #
    # @example Rails configuration
    #   Rails.application.config.active_job.queue_adapter = :shoryuken

    # Determine the appropriate base class based on Rails version
    # This prevents AbstractAdapter autoloading issues in Rails 7.0-7.1
    base = if defined?(Rails) && defined?(Rails::VERSION)
             (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR < 2 ? Object : AbstractAdapter)
           else
             Object
           end

    # Shoryuken queue adapter for ActiveJob integration.
    # Provides methods for enqueueing jobs to SQS queues.
    class ShoryukenAdapter < base
      class << self
        # Returns the singleton adapter instance
        #
        # @return [ShoryukenAdapter] the adapter instance
        def instance
          # https://github.com/ruby-shoryuken/shoryuken/pull/174#issuecomment-174555657
          @instance ||= new
        end

        # Enqueues a job for immediate processing
        #
        # @param job [ActiveJob::Base] the job to enqueue
        # @return [Aws::SQS::Types::SendMessageResult] the send result
        def enqueue(job)
          instance.enqueue(job)
        end

        # Enqueues a job for delayed processing
        #
        # @param job [ActiveJob::Base] the job to enqueue
        # @param timestamp [Float] Unix timestamp when the job should be processed
        # @return [Aws::SQS::Types::SendMessageResult] the send result
        def enqueue_at(job, timestamp)
          instance.enqueue_at(job, timestamp)
        end
      end

      # Checks if jobs should be enqueued after transaction commit (Rails 7.2+)
      #
      # @return [Boolean] always returns true
      def enqueue_after_transaction_commit?
        true
      end

      # Indicates whether Shoryuken is in the process of shutting down.
      #
      # This method is required for ActiveJob Continuations support (Rails 8.1+).
      # When true, it signals to jobs that they should checkpoint their progress
      # and gracefully interrupt execution to allow for resumption after restart.
      #
      # @return [Boolean] true if Shoryuken is shutting down, false otherwise
      # @see https://github.com/rails/rails/pull/55127 Rails ActiveJob Continuations
      def stopping?
        launcher = Shoryuken::Runner.instance.launcher
        launcher&.stopping? || false
      end

      # Enqueues a job for immediate processing
      #
      # @param job [ActiveJob::Base] the job to enqueue
      # @param options [Hash] SQS message configuration
      # @option options [Integer] :delay_seconds delay before the message becomes visible
      # @option options [String] :message_group_id FIFO queue group ID
      # @option options [String] :message_deduplication_id FIFO queue deduplication ID
      # @return [Aws::SQS::Types::SendMessageResult] the send result
      def enqueue(job, options = {}) # :nodoc:
        register_worker!(job)

        job.sqs_send_message_parameters.merge! options

        queue = Shoryuken::Client.queues(job.queue_name)
        send_message_params = message queue, job
        job.sqs_send_message_parameters = send_message_params
        queue.send_message send_message_params
      end

      # Enqueues a job for delayed processing
      #
      # @param job [ActiveJob::Base] the job to enqueue
      # @param timestamp [Float] Unix timestamp when the job should be processed
      # @return [Aws::SQS::Types::SendMessageResult] the send result
      # @raise [Shoryuken::Errors::FifoDelayNotSupportedError] if delay is used with a FIFO queue
      def enqueue_at(job, timestamp) # :nodoc:
        delay = calculate_delay(timestamp)

        # FIFO queues do not support per-message delays
        # Check early to fail synchronously (before any async wrapping in subclasses)
        # Note: negative delays (past timestamps) don't need handling here -
        # SQS treats them as immediate delivery (delay_seconds=0)
        # See https://github.com/ruby-shoryuken/shoryuken/issues/924
        if delay.positive?
          queue = Shoryuken::Client.queues(job.queue_name)
          if queue.fifo?
            raise Shoryuken::Errors::FifoDelayNotSupportedError,
                  "FIFO queue '#{queue.name}' does not support per-message delays. " \
                  'When using ActiveJob retry_on with FIFO queues, set `wait: 0`.'
          end
        end

        enqueue(job, delay_seconds: delay)
      end

      # Bulk enqueue multiple jobs efficiently using SQS batch API.
      # Called by ActiveJob.perform_all_later (Rails 7.1+).
      #
      # @param jobs [Array<ActiveJob::Base>] array of ActiveJob instances to be enqueued
      # @return [Integer] number of jobs successfully enqueued
      def enqueue_all(jobs) # :nodoc:
        jobs.group_by(&:queue_name).each do |queue_name, queue_jobs|
          queue = Shoryuken::Client.queues(queue_name)

          queue_jobs.each_slice(10) do |batch|
            entries = batch.map.with_index do |job, idx|
              register_worker!(job)
              msg = message(queue, job)
              job.sqs_send_message_parameters = msg
              { id: idx.to_s }.merge(msg)
            end

            response = queue.send_messages(entries: entries)
            successful_ids = response.successful.map { |r| r.id.to_i }.to_set
            batch.each_with_index do |job, idx|
              job.successfully_enqueued = successful_ids.include?(idx)
            end
          end
        end

        jobs.count(&:successfully_enqueued?)
      end

      private

      # Calculates the delay in seconds from a timestamp
      #
      # @param timestamp [Float] Unix timestamp
      # @return [Integer] delay in seconds
      # @raise [RuntimeError] if delay exceeds 15 minutes
      def calculate_delay(timestamp)
        delay = (timestamp - Time.current.to_f).round
        raise 'The maximum allowed delay is 15 minutes' if delay > 15.minutes

        delay
      end

      # Builds the SQS message parameters for a job
      #
      # @param queue [Shoryuken::Queue] the queue to send to
      # @param job [ActiveJob::Base] the job to serialize
      # @return [Hash] the message parameters
      def message(queue, job)
        body = job.serialize
        job_params = job.sqs_send_message_parameters

        attributes = job_params[:message_attributes] || {}

        msg = {
          message_body: body,
          message_attributes: attributes.merge(MESSAGE_ATTRIBUTES)
        }

        if queue.fifo?
          # See https://github.com/ruby-shoryuken/shoryuken/issues/457 and
          # https://github.com/ruby-shoryuken/shoryuken/pull/750#issuecomment-1781317929
          msg[:message_deduplication_id] = Digest::SHA256.hexdigest(
            JSON.dump(body.except('job_id', 'enqueued_at'))
          )
        end

        msg.merge(job_params.except(:message_attributes))
      end

      # Registers the JobWrapper as the worker for the job's queue
      #
      # @param job [ActiveJob::Base] the job being enqueued
      # @return [void]
      def register_worker!(job)
        Shoryuken.register_worker(job.queue_name, Shoryuken::ActiveJob::JobWrapper)
      end

      # Default message attributes identifying the Shoryuken worker class
      MESSAGE_ATTRIBUTES = {
        'shoryuken_class' => {
          string_value: Shoryuken::ActiveJob::JobWrapper.to_s,
          data_type: 'String'
        }
      }.freeze
    end
  end
end

queues

      def queues(name)
        @@queues[name.to_s] ||= Shoryuken::Queue.new(sqs, name)
      end


def enqueue_all(jobs) # :nodoc:
        jobs.group_by(&:queue_name).each do |queue_name, queue_jobs|
          queue = Shoryuken::Client.queues(queue_name)

          queue_jobs.each_slice(10) do |batch|
            entries = batch.map.with_index do |job, idx|
              register_worker!(job)
              msg = message(queue, job)
              job.sqs_send_message_parameters = msg
              { id: idx.to_s }.merge(msg)
            end

            response = queue.send_messages(entries: entries)
            successful_ids = response.successful.map { |r| r.id.to_i }.to_set
            batch.each_with_index do |job, idx|
              job.successfully_enqueued = successful_ids.include?(idx)
            end
          end
        end

        jobs.count(&:successfully_enqueued?)
      end

      def register_worker!(job)
        Shoryuken.register_worker(job.queue_name, Shoryuken::ActiveJob::JobWrapper)
      end

message

      def message(queue, job)
        body = job.serialize
        job_params = job.sqs_send_message_parameters

        attributes = job_params[:message_attributes] || {}

        msg = {
          message_body: body,
          message_attributes: attributes.merge(MESSAGE_ATTRIBUTES)
        }

        if queue.fifo?
          # See https://github.com/ruby-shoryuken/shoryuken/issues/457 and
          # https://github.com/ruby-shoryuken/shoryuken/pull/750#issuecomment-1781317929
          msg[:message_deduplication_id] = Digest::SHA256.hexdigest(
            JSON.dump(body.except('job_id', 'enqueued_at'))
          )
        end

        msg.merge(job_params.except(:message_attributes))
      end

    def send_messages(options)
      client.send_message_batch(sanitize_messages!(options).merge(queue_url: url))
    end


def message(queue, job)
        body = job.serialize
        job_params = job.sqs_send_message_parameters

        attributes = job_params[:message_attributes] || {}

        msg = {
          message_body: body,
          message_attributes: attributes.merge(MESSAGE_ATTRIBUTES)
        }

        if queue.fifo?
          # See https://github.com/ruby-shoryuken/shoryuken/issues/457 and
          # https://github.com/ruby-shoryuken/shoryuken/pull/750#issuecomment-1781317929
          msg[:message_deduplication_id] = Digest::SHA256.hexdigest(
            JSON.dump(body.except('job_id', 'enqueued_at'))
          )
        end

        msg.merge(job_params.except(:message_attributes))
      end


# frozen_string_literal: true

module ROM
  # Helper module for dataset classes
  #
  # It provides a constructor accepting data, header and an optional row_proc.
  # This module is used internally by EnumerableDataset and ArrayDataset.
  #
  # @private
  module DataProxy
    NON_FORWARDABLE = %i[
      each to_a to_ary kind_of? instance_of? is_a?
    ].freeze

    # Wrapped data array
    #
    # @return [Object] Data object for the iterator
    #
    # @api private
    attr_reader :data

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

    # Extends the class with `forward` DSL and Equalizer using `data` attribute
    #
    # @see ClassMethods#forward
    #
    # @api private
    def self.included(klass)
      klass.class_eval do
        extend ClassMethods

        include ::Dry::Equalizer(:data)

        option :row_proc, default: -> { self.class.row_proc }
      end
    end

    # Iterate over data using row_proc
    #
    # @return [Enumerator] if block is not given
    #
    # @api private
    def each
      return to_enum unless block_given?

      data.each { |tuple| yield(row_proc[tuple]) }
    end

    module ClassMethods
      # Default no-op tuple proc
      #
      # @return [Proc]
      #
      # @api private
      def row_proc = -> tuple { tuple }

      # Forward provided methods to the underlaying data object
      #
      # @example
      #
      #   class MyDataset
      #     include DataProxy
      #
      #     forward(:find_all, :map)
      #   end
      #
      # @return [undefined]
      #
      # @api public
      def forward(*methods)
        # FIXME: we should probably raise if one of the non-forwardable methods
        #       was provided
        (methods - NON_FORWARDABLE).each do |method_name|
          class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
            def #{method_name}(...)                              # def find_all(...)
              response = data.public_send(:#{method_name}, ...)  #   response = data.public_send(:find_all, ...)
                                                                 #
              if response.equal?(data)                           #   if response.equal?(data)
                self                                             #     self
              elsif response.is_a?(data.class)                   #   elsif response.is_a?(data.class)
                self.class.new(response)                         #     self.class.new(response)
              else                                               #   else
                response                                         #     response
              end                                                #   end
            end                                                  # end
          RUBY
        end
      end
    end
  end
end


# frozen_string_literal: true

require 'rom/configuration_dsl/relation'
require 'rom/configuration_dsl/command_dsl'
require 'rom/configuration_dsl/mapper_dsl'

module ROM
  # This extends Configuration class with the DSL methods
  #
  # @api public
  module ConfigurationDSL
    # Relation definition DSL
    #
    # @example
    #   setup.relation(:users) do
    #     def names
    #       project(:name)
    #     end
    #   end
    #
    # @api public
    def relation(name, options = EMPTY_HASH, &)
      klass_opts = { adapter: default_adapter }.merge(options)
      klass = Relation.build_class(name, klass_opts)
      klass.schema_opts(dataset: name, relation: name)
      klass.class_eval(&) if block_given?
      register_relation(klass)
      klass
    end

    # Command definition DSL
    #
    # @example
    #   setup.commands(:users) do
    #     define(:create) do
    #       input NewUserParams
    #       result :one
    #     end
    #
    #     define(:update) do
    #       input UserParams
    #       result :many
    #     end
    #
    #     define(:delete) do
    #       result :many
    #     end
    #   end
    #
    # @api public
    def commands(name, &)
      register_command(*CommandDSL.new(name, default_adapter, &).command_classes)
    end

    # Mapper definition DSL
    #
    # @api public
    def mappers(&block)
      register_mapper(*MapperDSL.new(self, mapper_classes, block).mapper_classes)
    end

    # Configures a plugin for a specific adapter to be enabled for all relations
    #
    # @example
    #   config = ROM::Configuration.new(:sql, 'sqlite::memory')
    #
    #   config.plugin(:sql, relations: :instrumentation) do |p|
    #     p.notifications = MyNotificationsBackend
    #   end
    #
    #   config.plugin(:sql, relations: :pagination)
    #
    # @param [Symbol] adapter The adapter identifier
    # @param [Hash<Symbol=>Symbol>] spec Component identifier => plugin identifier
    #
    # @return [Plugin]
    #
    # @api public
    def plugin(adapter, spec, &)
      type, name = spec.flatten(1)
      plugin = plugin_registry[type].adapter(adapter).fetch(name) do
        plugin_registry[type].fetch(name)
      end

      if block_given?
        register_plugin(plugin.configure(&))
      else
        register_plugin(plugin)
      end
    end

    # @api private
    def plugin_registry = ::ROM.plugin_registry
  end
end


# frozen_string_literal: true

require 'rom/constants'
require 'rom/initializer'
require 'rom/mapper'
require 'rom/struct'
require 'rom/struct_compiler'
require 'rom/cache'

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

    defines :mapper_options

    mapper_options(EMPTY_HASH)

    option :cache, default: -> { Cache.new }

    attr_reader :struct_compiler

    attr_reader :mapper_options

    def initialize(*, **)
      super
      @struct_compiler = StructCompiler.new(cache: cache)
      @cache = cache.namespaced(:mappers)
      @mapper_options = self.class.mapper_options
    end

    def call(ast)
      cache.fetch_or_store(ast.hash) { Mapper.build(Header.coerce(*visit(ast))) }
    end
    alias_method :[], :call

    private

    def visit(node)
      name, node = node
      __send__("visit_#{name}", node)
    end

    # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
    def visit_relation(node)
      rel_name, header, meta_options = node
      name = meta_options[:combine_name] || meta_options[:alias] || rel_name
      namespace = meta_options.fetch(:struct_namespace)

      model = meta_options.fetch(:model) do
        if meta_options[:combine_name]
          false
        else
          struct_compiler[name, header, namespace]
        end
      end

      options = [header.map(&method(:visit)), mapper_options.merge(model: model)]

      if meta_options[:combine_type]
        type = meta_options[:combine_type] == :many ? :array : :hash
        keys = meta_options.fetch(:keys)

        [name, combine: true, type: type, keys: keys, header: Header.coerce(*options)]
      elsif meta_options[:wrap]
        [name, wrap: true, type: :hash, header: Header.coerce(*options)]
      else
        options
      end
    end
    # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity

    def visit_attribute(node)
      name, _, meta_options = node

      if meta_options[:alias]
        [meta_options[:alias], from: name]
      else
        [name]
      end
    end
  end
end

coerce

    def self.coerce(input, options = {})
      if input.instance_of?(self)
        input
      else
        attributes = input.to_h { [_1.first, Attribute.coerce(_1)] }

        new(attributes, options)
      end
    end


# frozen_string_literal: true

require 'rom/support/configurable'
require 'rom/gateway'

module ROM
  # Core gateway configuration interface
  #
  # @api private
  class Environment
    include Configurable

    attr_reader :gateways

    attr_reader :gateways_map

    # @api private
    def initialize(*args)
      @gateways = {}
      @gateways_map = {}

      configure_gateways(*args) unless args.empty?
    end

    private

    # @api private
    def configure_gateways(*args)
      normalized_gateway_args = normalize_gateway_args(args)
      normalized_gateways = normalize_gateways(normalized_gateway_args)

      @gateways, @gateways_map = normalized_gateways.values_at(:gateways, :map)

      normalized_gateway_args.each_with_object(config) do |(name, gateway_config), config|
        options = gateway_config.is_a?(Array) && gateway_config.last
        load_config(config.gateways[name], options) if options.is_a?(Hash)
      end
    end

    # @api private
    def normalize_gateway_args(args)
      if args.first.is_a?(::Hash)
        args.first
      else
        { default: args }
      end
    end

    # Build gateways using the setup interface
    #
    # @api private
    def normalize_gateways(gateways_config)
      gateways_config.each_with_object(map: {}, gateways: {}) do |(name, spec), hash|
        identifier, *args = Array(spec)

        if identifier.is_a?(Gateway)
          gateway = identifier
        else
          kwargs = args.last.is_a?(::Hash) ? args.pop : {}
          gateway = Gateway.setup(identifier, *args.flatten, **kwargs)
        end

        hash[:map][gateway] = name
        hash[:gateways][name] = gateway
      end
    end

    # @api private
    def load_config(config, hash)
      hash.each do |key, value|
        if value.is_a?(Hash)
          load_config(config[key], value)
        else
          config.send("#{key}=", value)
        end
      end
    end
  end
end

setup

    def self.setup(gateway_or_scheme, *args, **kwargs)
      case gateway_or_scheme
      when String
        raise ArgumentError, <<-STRING.gsub(/^ {10}/, '')
          URIs without an explicit scheme are not supported anymore.
          See https://github.com/rom-rb/rom/blob/master/CHANGELOG.md
        STRING
      when Symbol
        klass = class_from_symbol(gateway_or_scheme)

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

        gateway_or_scheme
      end
    end


# frozen_string_literal: true

require 'rom/types'
require 'rom/initializer'
require 'rom/pipeline'

require 'rom/commands/class_interface'
require 'rom/commands/composite'
require 'rom/commands/graph'
require 'rom/commands/lazy'

module ROM
  # Abstract command class
  #
  # Provides a constructor accepting relation with options and basic behavior
  # for calling, currying and composing commands.
  #
  # Typically command subclasses should inherit from specialized
  # Create/Update/Delete, not this one.
  #
  # @abstract
  #
  # @api public
  class Command
    extend Dry::Core::ClassAttributes
    extend Initializer
    extend ClassInterface

    include Dry::Equalizer(:relation, :options)
    include Commands
    include Pipeline::Operator

    # @!method self.adapter
    #   Get or set adapter identifier
    #
    #   @overload adapter
    #     Get adapter identifier
    #
    #     @example
    #       ROM::Memory::Commands::Create.adapter
    #       # => :memory
    #
    #     @return [Symbol]
    #
    #   @overload adapter(identifier)
    #     Set adapter identifier. This must always match actual adapter identifier
    #     that was used to register an adapter.
    #
    #     @example
    #       module MyAdapter
    #         class CreateCommand < ROM::Commands::Memory::Create
    #           adapter :my_adapter
    #         end
    #       end
    #
    # @api public
    defines :adapter

    # @!method self.relation
    #   Get or set relation identifier
    #
    #   @overload relation
    #     Get relation identifier
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         relation :users
    #       end
    #
    #       CreateUser.relation
    #       # => :users
    #
    #     @return [Symbol]
    #
    #   @overload relation(identifier)
    #     Set relation identifier.
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         relation :users
    #       end
    #
    # @api public
    defines :relation

    # @!method self.result
    #   Get or set result type
    #
    #   @overload result
    #     Get result type
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         result :one
    #       end
    #
    #       CreateUser.result
    #       # => :one
    #
    #     @return [Symbol]
    #
    #   @overload result(identifier)
    #     Set result type
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         result :one
    #       end
    #
    # @api public
    defines :result

    # @!method self.input
    #   Get or set input processing function. This is typically set during setup
    #   to relation's input_schema
    #
    #   @overload input
    #     Get input processing function
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         input -> tuple { .. }
    #       end
    #
    #       CreateUser.input
    #       # Your custom function
    #
    #     @return [Proc,#call]
    #
    #   @overload input(identifier)
    #     Set input processing function
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         input -> tuple { .. }
    #       end
    #
    # @api public
    defines :input

    # @!method self.register_as
    #   Get or set identifier that should be used to register a command in a container
    #
    #   @overload register_as
    #     Get registration identifier
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         register_as :create_user
    #       end
    #
    #       CreateUser.register_as
    #       # => :create_user
    #
    #     @return [Symbol]
    #
    #   @overload register_as(identifier)
    #     Set registration identifier
    #
    #     @example
    #       class CreateUser < ROM::Commands::Create[:memory]
    #         register_as :create_user
    #       end
    #
    # @api public
    defines :register_as

    # @!method self.restrictable
    #   @overload restrictable
    #     Check if a command class is restrictable
    #
    #     @example
    #       class UpdateUser < ROM::Commands::Update[:memory]
    #         restrictable true
    #       end
    #
    #       CreateUser.restrictable
    #       # => true
    #
    #     @return [FalseClass, TrueClass]
    #
    #   @overload restrictable(value)
    #     Set if a command is restrictable
    #
    #     @example
    #       class UpdateUser < ROM::Commands::Update[:memory]
    #         restrictable true
    #       end
    #
    # @api public
    defines :restrictable

    # @!attribute [r] relation
    #   @return [Relation] Command's relation
    param :relation

    CommandType = Types::Strict::Symbol.enum(:create, :update, :delete)
    Result = Types::Strict::Symbol.enum(:one, :many)

    # @!attribute [r] type
    #   @return [Symbol] The command type, one of :create, :update or :delete
    option :type, type: CommandType, optional: true

    # @!attribute [r] source
    #   @return [Relation] The source relation
    option :source, default: -> { relation }

    # @!attribute [r] result
    #   @return [Symbol] Result type, either :one or :many
    option :result, type: Result

    # @!attribute [r] input
    #   @return [Proc, #call] Tuple processing function, typically uses Relation#input_schema
    option :input

    # @!attribute [r] curry_args
    #   @return [Array] Curried args
    option :curry_args, default: -> { EMPTY_ARRAY }

    # @!attribute [r] before
    #   @return [Array<Hash>] An array with before hooks configuration
    option :before, Types::Coercible::Array, reader: false, default: -> { self.class.before }

    # @!attribute [r] after
    #   @return [Array<Hash>] An array with after hooks configuration
    option :after, Types::Coercible::Array, reader: false, default: -> { self.class.after }

    input Hash
    result :many

    # Return name of this command's relation
    #
    # @return [ROM::Relation::Name]
    #
    # @api public
    def name
      relation.name
    end

    # Return gateway of this command's relation
    #
    # @return [Symbol]
    #
    # @api public
    def gateway
      relation.gateway
    end

    # Execute the command
    #
    # @abstract
    #
    # @return [Array] an array with inserted tuples
    #
    # @api private
    def execute(*)
      raise(
        ::NotImplementedError,
        "#{self.class}##{__method__} must be implemented"
      )
    end

    # Call the command and return one or many tuples
    #
    # This method will apply before/after hooks automatically
    #
    # @api public
    #
    # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
    def call(*args, &)
      tuples =
        if hooks?
          prepared =
            if curried?
              apply_hooks(before_hooks, *(curry_args + args))
            else
              apply_hooks(before_hooks, *args)
            end

          result =
            if prepared
              execute(prepared, &)
            else
              execute(&)
            end

          if curried?
            if !args.empty?
              apply_hooks(after_hooks, result, *args)
            elsif curry_args.size > 1
              apply_hooks(after_hooks, result, curry_args[1])
            else
              apply_hooks(after_hooks, result)
            end
          else
            apply_hooks(after_hooks, result, *args.drop(1))
          end
        else
          execute(*(curry_args + args), &)
        end

      if one?
        tuples.first
      else
        tuples
      end
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

    alias_method :[], :call

    # Curry this command with provided args
    #
    # Curried command can be called without args. If argument is a graph input processor,
    # lazy command will be returned, which is used for handling nested input hashes.
    #
    # @return [Command, Lazy]
    #
    # @api public
    def curry(*args)
      if curry_args.empty? && args.first.is_a?(Graph::InputEvaluator)
        Lazy[self].new(self, *args)
      else
        self.class.build(relation, **options, curry_args: args)
      end
    end

    # Compose this command with other commands
    #
    # Composed commands can handle nested input
    #
    # @return [Command::Graph]
    #
    # @api public
    def combine(*others) = Graph.new(self, others)

    # Check if this command is curried
    #
    # @return [TrueClass, FalseClass]
    #
    # @api public
    def curried? = !curry_args.empty?

    # Return a new command with appended before hooks
    #
    # @param [Array<Hash>] hooks A list of before hooks configurations
    #
    # @return [Command]
    #
    # @api public
    def before(*hooks)
      self.class.new(relation, **options, before: before_hooks + hooks)
    end

    # Return a new command with appended after hooks
    #
    # @param [Array<Hash>] hooks A list of after hooks configurations
    #
    # @return [Command]
    #
    # @api public
    def after(*hooks)
      self.class.new(relation, **options, after: after_hooks + hooks)
    end

    # List of before hooks
    #
    # @return [Array]
    #
    # @api public
    def before_hooks = options[:before]

    # List of after hooks
    #
    # @return [Array]
    #
    # @api public
    def after_hooks = options[:after]

    # Return a new command with other source relation
    #
    # This can be used to restrict command with a specific relation
    #
    # @return [Command]
    #
    # @api public
    def new(new_relation)
      self.class.build(new_relation, **options, source: relation)
    end

    # Check if this command has any hooks
    #
    # @api private
    def hooks? = !before_hooks.empty? || !after_hooks.empty?

    # Check if this command is lazy
    #
    # @return [false]
    #
    # @api private
    def lazy? = false

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

    # Check if this command returns a single tuple
    #
    # @return [TrueClass,FalseClass]
    #
    # @api private
    def one? = result.equal?(:one)

    # Check if this command returns many tuples
    #
    # @return [TrueClass,FalseClass]
    #
    # @api private
    def many? = result.equal?(:many)

    # Check if this command is restrictible through relation
    #
    # @return [TrueClass,FalseClass]
    #
    # @api private
    def restrictible? = self.class.restrictable.equal?(true)

    # Yields tuples for insertion or return an enumerator
    #
    # @api private
    def map_input_tuples(tuples, &mapper)
      return enum_for(:with_input_tuples, tuples) unless mapper

      if tuples.respond_to? :merge
        mapper[tuples]
      else
        tuples.map(&mapper)
      end
    end

    private

    # Hook called by Pipeline to get composite class for commands
    #
    # @return [Class]
    #
    # @api private
    def composite_class = Command::Composite

    # Apply provided hooks
    #
    # Used by #call
    #
    # @return [Array<Hash>]
    #
    # @api private
    def apply_hooks(hooks, tuples, *args)
      hooks.reduce(tuples) do |a, e|
        if e.is_a?(Hash)
          hook_meth, hook_args = e.to_a.flatten(1)
          __send__(hook_meth, a, *args, **hook_args)
        else
          __send__(e, a, *args)
        end
      end
    end

    # Pipes a dataset through command's relation
    #
    # @return [Array]
    #
    # @api private
    def wrap_dataset(dataset)
      if relation.is_a?(Relation::Composite)
        relation.new(dataset).to_a
      else
        dataset
      end
    end
  end
end


def apply_hooks(hooks, tuples, *args)
      hooks.reduce(tuples) do |a, e|
        if e.is_a?(Hash)
          hook_meth, hook_args = e.to_a.flatten(1)
          __send__(hook_meth, a, *args, **hook_args)
        else
          __send__(e, a, *args)
        end
      end
    end


# frozen_string_literal: true

require 'rom/support/inflector'

require 'rom/initializer'
require 'rom/types'
require 'rom/cache'
require 'rom/struct'
require 'rom/open_struct'
require 'rom/attribute'

module ROM
  # @api private
  class StructCompiler < Dry::Types::Compiler
    extend Initializer

    param :registry, default: -> { Dry::Types }
    option :cache, default: -> { Cache.new }

    # @api private
    def initialize(*, **)
      super
      @cache = cache.namespaced(:structs)
    end

    # Build a struct class based on relation header ast
    #
    # @api private
    def call(*args)
      cache.fetch_or_store(args) do
        name, header, ns = args
        attributes = header.map(&method(:visit)).compact

        if attributes.empty?
          ROM::OpenStruct
        else
          build_class(name, ROM::Struct, ns) do |klass|
            klass.attributes(attributes.to_h)
          end
        end
      end
    end
    alias_method :[], :call

    private

    # @api private
    def visit_relation(node)
      _, header, meta = node
      name = meta[:combine_name] || meta[:alias]
      namespace = meta.fetch(:struct_namespace)

      model = meta[:model] || call(name, header, namespace)

      member =
        if model < Dry::Struct
          model
        else
          Dry::Types::Nominal.new(model).constructor(&model.method(:new))
        end

      if meta[:combine_type] == :many
        [name, Types::Array.of(member)]
      else
        [name, member.optional]
      end
    end

    # @api private
    def visit_attribute(node)
      name, type, meta = node

      [meta[:alias] && !meta[:wrapped] ? meta[:alias] : name, visit(type).meta(meta)]
    end

    # @api private
    def visit_constructor(node)
      definition, * = node

      visit(definition)
    end

    # @api private
    def visit_constrained(node)
      definition, = node

      visit(definition)
    end

    # @api private
    def visit_enum(node)
      type_node, * = node
      visit(type_node)
    end

    # @api private
    def build_class(name, parent, ns, &)
      Dry::Core::ClassBuilder
        .new(name: class_name(name), parent: parent, namespace: ns)
        .call(&)
    end

    # @api private
    def class_name(name) = Inflector.classify(name)
  end
end

class StructCompiler < < Dry::Types::Compiler


def visit_relation(node)
      _, header, meta = node
      name = meta[:combine_name] || meta[:alias]
      namespace = meta.fetch(:struct_namespace)

      model = meta[:model] || call(name, header, namespace)

      member =
        if model < Dry::Struct
          model
        else
          Dry::Types::Nominal.new(model).constructor(&model.method(:new))
        end

      if meta[:combine_type] == :many
        [name, Types::Array.of(member)]
      else
        [name, member.optional]
      end
    end

call

    def call(*args)
      cache.fetch_or_store(args) do
        name, header, ns = args
        attributes = header.map(&method(:visit)).compact

        if attributes.empty?
          ROM::OpenStruct
        else
          build_class(name, ROM::Struct, ns) do |klass|
            klass.attributes(attributes.to_h)
          end
        end
      end
    end


# frozen_string_literal: true

require 'forwardable'

require 'rom/environment'
require 'rom/setup'
require 'rom/configuration_dsl'
require 'rom/support/notifications'

module ROM
  class Configuration
    extend Forwardable
    extend Notifications

    register_event('configuration.relations.class.ready')
    register_event('configuration.relations.object.registered')
    register_event('configuration.relations.registry.created')
    register_event('configuration.relations.schema.allocated')
    register_event('configuration.relations.schema.set')
    register_event('configuration.relations.dataset.allocated')
    register_event('configuration.commands.class.before_build')

    include ::ROM::ConfigurationDSL

    NoDefaultAdapterError = ::Class.new(::StandardError)

    # @!attribute [r] environment
    #   @return [Environment] Environment object with gateways
    attr_reader :environment

    # @!attribute [r] setup
    #   @return [Setup] Setup object which collects component classes and plugins
    attr_reader :setup

    # @!attribute [r] notifications
    #   @return [Notifications] Notification bus instance
    attr_reader :notifications

    def_delegators :@setup, :register_relation, :register_command,
                   :register_mapper, :register_plugin,
                   :command_classes, :mapper_classes,
                   :auto_registration

    def_delegators :@environment, :gateways, :gateways_map, :configure, :config

    # Initialize a new configuration
    #
    # @see Environment#initialize
    #
    # @return [Configuration]
    #
    # @api private
    def initialize(*args)
      @environment = Environment.new(*args)
      @notifications = Notifications.event_bus(:configuration)
      @setup = Setup.new(notifications)

      yield self if block_given?
    end

    # Apply a plugin to the configuration
    #
    # @param [Mixed] plugin The plugin identifier, usually a Symbol
    # @param [Hash] options Plugin options
    #
    # @return [Configuration]
    #
    # @api public
    def use(plugin, options = EMPTY_HASH)
      if plugin.is_a?(::Array)
        plugin.each { |p| use(p) }
      elsif plugin.is_a?(::Hash)
        plugin.to_a.each { |p| use(*p) }
      else
        ROM.plugin_registry[:configuration].fetch(plugin).apply_to(self, options)
      end

      self
    end

    # Return gateway identified by name
    #
    # @return [Gateway]
    #
    # @api private
    def [](name) = gateways.fetch(name)

    # Hook for respond_to? used internally
    #
    # @api private
    def respond_to?(name, ...) = gateways.key?(name) || super

    # @api private
    def default_gateway
      @default_gateway ||= gateways[:default]
    end

    # @api private
    def adapter_for_gateway(gateway)
      ROM.adapters.select do |_key, value|
        value.const_defined?(:Gateway) && gateway.is_a?(value.const_get(:Gateway))
      end.keys.first
    end

    # @api private
    def relation_classes(gateway = nil)
      if gateway
        gw_name = gateway.is_a?(Symbol) ? gateway : gateways_map[gateway]
        setup.relation_classes.select { |rel| rel.gateway == gw_name }
      else
        setup.relation_classes
      end
    end

    # @api private
    def default_adapter
      @default_adapter ||= adapter_for_gateway(default_gateway) || ROM.adapters.keys.first
    end

    private

    # @api private
    def respond_to_missing?(name, ...) = gateways.key?(name) || super

    # Returns gateway if method is a name of a registered gateway
    #
    # @return [Gateway]
    #
    # @api private
    def method_missing(name, ...) = gateways.fetch(name) { super }
  end
end


# frozen_string_literal: true

module ROM
  # Relation extension which provides auto-currying of relation view methods
  #
  # @api private
  module AutoCurry
    # @api private
    class Wrapper < ::Module
      def initialize(name, arity, &block)
        define_method(name) do |*args, **kwargs, &mblock|
          kwargs_size =
            if kwargs.empty?
              0
            else
              1
            end

          response =
            if arity.negative? || arity.eql?(args.size + kwargs_size)
              super(*args, **kwargs, &mblock)
            else
              self.class.curried.new(self, view: name, curry_args: args, arity: arity)
            end

          if block
            response.instance_exec(&block)
          else
            response
          end
        end
      end
    end

    def self.extended(klass)
      klass.define_singleton_method(:method_added) do |name|
        return if auto_curry_busy?

        auto_curry_guard { auto_curry(name) }
        super(name)
      end
    end

    # @api private
    def auto_curry_guard
      @__auto_curry_busy__ = true
      yield
    ensure
      @__auto_curry_busy__ = false
    end

    # @api private
    def auto_curry_busy?
      @__auto_curry_busy__ ||= false
    end

    # @api private
    def auto_curried_methods
      @__auto_curried_methods__ ||= Set.new
    end

    # Auto-curry a method
    #
    # @param [Symbol] name The name of a method
    #
    # @api private
    def auto_curry(name, &)
      arity = instance_method(name).arity

      if public_instance_methods.include?(name) && arity != 0
        mod = Wrapper.new(name, arity, &)

        auto_curried_methods << name

        prepend(mod)
      else
        self
      end
    end
  end
end


def auto_curry(name, &)
      arity = instance_method(name).arity

      if public_instance_methods.include?(name) && arity != 0
        mod = Wrapper.new(name, arity, &)

        auto_curried_methods << name

        prepend(mod)
      else
        self
      end
    end


# frozen_string_literal: true

require 'concurrent/map'

require 'rom/constants'
require 'rom/registry'

module ROM
  # Specialized registry class for commands
  #
  # @api public
  class CommandRegistry < Registry
    # Internal command registry
    #
    # @return [Registry]
    #
    # @api private
    param :elements

    # @!attribute [r] relation_name
    #   @return [Relation::Name] The name of a relation
    option :relation_name

    # @!attribute [r] mappers
    #   @return [MapperRegistry] Optional mapper registry
    option :mappers, optional: true

    # @!attribute [r] mapper
    #   @return [Object#call] Default mapper for processing command results
    option :mapper, optional: true

    # @!attribute [r] compiler
    #   @return [CommandCompiler] A command compiler instance
    option :compiler, optional: true

    # @api private
    def self.element_not_found_error = CommandNotFoundError

    # Return a command from the registry
    #
    # If mapper is set command will be turned into a composite command with
    # auto-mapping
    #
    # @overload [](name)
    #   @param [Symbol] name The command identifier from the registry
    #   @example
    #     create_user = rom.commands[:users][:create]
    #     create_user[name: 'Jane']
    #
    #     # with mapping, assuming :entity mapper is registered for :users relation
    #     create_user = rom.commands[:users].map_with(:entity)[:create]
    #     create_user[name: 'Jane'] # => result is sent through :entity mapper
    #
    # @overload [](*args)
    #   @param [Array] *args {CommandCompiler} arguments
    #   @see CommandCompiler#call
    #
    # @return [Command,Command::Composite]
    #
    # @api public
    def [](*args)
      if args.size.equal?(1)
        command = super
        mapper = options[:mapper]

        if mapper
          command.curry >> mapper
        else
          command
        end
      else
        cache.fetch_or_store(args.hash) { compiler.(*args) }
      end
    end

    # Specify a mapper that should be used for commands from this registry
    #
    # @example
    #   entity_commands = rom.commands[:users].map_with(:entity)
    #
    #
    # @param [Symbol] mapper_name The name of a registered mapper
    #
    # @return [CommandRegistry]
    #
    # @api public
    def map_with(mapper_name) = with(mapper: mappers[mapper_name])

    # @api private
    def set_compiler(compiler) # rubocop:disable Naming/AccessorMethodName
      options[:compiler] = @compiler = compiler
    end

    # @api private
    def set_mappers(mappers) # rubocop:disable Naming/AccessorMethodName
      options[:mappers] = @mappers = mappers
    end

    private

    # Allow checking if a certain command is available using dot-notation
    #
    # @api private
    def respond_to_missing?(name, include_private = false)
      key?(name) || super
    end

    # Allow retrieving commands using dot-notation
    #
    # @api private
    def method_missing(name, *)
      if key?(name)
        self[name]
      else
        super
      end
    end
  end
end

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

    # @api private
    def size = objects.size

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


# frozen_string_literal: true

require 'rom/support/inflector'

module ROM
  # Model builders can be used to build model classes for mappers
  #
  # This is used when you define a mapper and setup a model using :name option.
  #
  # @example
  #   # this will define User model for you
  #   class UserMapper < ROM::Mapper
  #     model name: 'User'
  #     attribute :id
  #     attribute :name
  #   end
  #
  # @private
  class ModelBuilder
    attr_reader :name

    attr_reader :const_name

    attr_reader :namespace

    attr_reader :klass

    # Return model builder subclass based on type
    #
    # @param [Symbol] type
    #
    # @return [Class]
    #
    # @api private
    def self.[](type)
      case type
      when :poro then PORO
      else
        raise ::ArgumentError, "#{type.inspect} is not a supported model type"
      end
    end

    # Build a model class
    #
    # @return [Class]
    #
    # @api private
    def self.call(*args) = new(*args).call

    # @api private
    def initialize(options = {})
      @name = options[:name]

      if name
        parts = name.split('::')

        @const_name = parts.pop

        @namespace =
          if parts.any?
            Inflector.constantize(parts.join('::'))
          else
            ::Object
          end
      end
    end

    # Define a model class constant
    #
    # @api private
    def define_const = namespace.const_set(const_name, klass)

    # Build a model class supporting specific attributes
    #
    # @return [Class]
    #
    # @api private
    def call(attrs)
      define_class(attrs)
      define_const if const_name
      @klass
    end

    # PORO model class builder
    #
    # @private
    class PORO < ModelBuilder
      def define_class(attrs)
        @klass = ::Class.new

        @klass.attr_reader(*attrs)

        @klass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
          def initialize(params)                                             # def initialize(params)
            #{attrs.map { |name| "@#{name} = params[:#{name}]" }.join("\n")} #   @name = params[:name]
          end                                                                # end
        RUBY

        self
      end
    end
  end
end