# frozen_string_literal: true

require_relative "constants"
require_relative "errors"

module Hanami
  # Calls `configure_for_slice(slice)` on the extended class whenever it is first
  # subclassed within a module namespace corresponding to a slice.
  #
  # @example
  #   class BaseClass
  #     extend Hanami::SliceConfigurable
  #   end
  #
  #   # slices/main/lib/my_class.rb
  #   module Main
  #     class MyClass < BaseClass
  #       # Will be called with `Main::Slice`
  #       def self.configure_for_slice(slice)
  #         # ...
  #       end
  #     end
  #   end
  #
  # @api private
  # @since 2.0.0
  module SliceConfigurable
    class << self
      def extended(klass)
        slice_for = method(:slice_for)

        inherited_mod = Module.new do
          define_method(:inherited) do |subclass|
            unless Hanami.app?
              raise ComponentLoadError, "Class #{klass} must be defined within an Hanami app"
            end

            super(subclass)

            subclass.instance_variable_set(:@configured_for_slices, configured_for_slices.dup)

            slice = slice_for.(subclass)
            return unless slice

            unless subclass.configured_for_slice?(slice)
              subclass.configure_for_slice(slice) if subclass.respond_to?(:configure_for_slice)
              subclass.configured_for_slices << slice
            end
          end
        end

        klass.singleton_class.prepend(inherited_mod)
      end

      private

      def slice_for(klass)
        return unless klass.name

        slices = Hanami.app.slices.with_nested + [Hanami.app]

        slices.detect { |slice| klass.name.start_with?("#{slice.namespace}#{MODULE_DELIMITER}") }
      end
    end

    def configured_for_slice?(slice)
      configured_for_slices.include?(slice)
    end

    def configured_for_slices
      @configured_for_slices ||= []
    end
  end
end


# frozen_string_literal: true

require "zeitwerk"
require "dry/system"

require_relative "constants"
require_relative "errors"

module Hanami
  # A slice represents any distinct area of concern within an Hanami app.
  #
  # For smaller apps, a slice may encompass the whole app itself (see
  # {Hanami::App}), whereas larger apps may consist of many slices.
  #
  # Each slice corresponds a single module namespace and a single root directory of source
  # files for loading as components into its container.
  #
  # Each slice has its own config, and may optionally have its own settings, routes, as well as
  # other nested slices.
  #
  # Slices expect an Hanami app to be defined (which itself is a slice). They will initialize their
  # config as a copy of the app's, and will also configure certain components
  #
  # Slices must be _prepared_ and optionally _booted_ before they can be used (see
  # {ClassMethods.prepare} and {ClassMethods.boot}). A prepared slice will lazily load its
  # components and nested slices (useful for minimising initial load time), whereas a
  # booted slice will eagerly load all its components and nested slices, then freeze its
  # container.
  #
  # @since 2.0.0
  class Slice
    @_mutex = Mutex.new

    # @api private
    def self.inherited(subclass)
      super

      subclass.extend(ClassMethods)

      @_mutex.synchronize do
        subclass.class_eval do
          @_mutex = Mutex.new
          @autoloader = Zeitwerk::Loader.new
          @container = Class.new(Dry::System::Container)
        end
      end
    end

    module ClassMethods
      # Returns the slice's parent.
      #
      # For top-level slices defined in `slices/` or `config/slices/`, this will be the Hanami app
      # itself (`Hanami.app`). For nested slices, this will be the slice in which they were
      # registered.
      #
      # @return [Hanami::Slice]
      #
      # @see #register_slice
      #
      # @api public
      # @since 2.0.0
      attr_reader :parent

      # Returns the slice's autoloader.
      #
      # Each slice has its own `Zeitwerk::Loader` autoloader instance, which is setup when the slice
      # is {#prepare prepared}.
      #
      # @return [Zeitwerk::Loader]
      #
      # @see https://github.com/fxn/zeitwerk
      #
      # @api public
      # @since 2.0.0
      attr_reader :autoloader

      # Returns the slice's container.
      #
      # This is a `Dry::System::Container` that is already configured for the slice.
      #
      # In ordinary usage, you shouldn't need direct access the container at all, since the slice
      # provides its own methods for interacting with the container (such as {#[]}, {#keys}, {#key?}
      # {#register}, {#register_provider}, {#prepare}, {#start}, {#stop}).
      #
      # If you need to configure the container directly, use {#prepare_container}.
      #
      # @see https://dry-rb.org/gems/dry-system
      #
      # @api public
      # @since 2.0.0
      attr_reader :container

      # Returns the Hanami app.
      #
      # @return [Hanami::App]
      #
      # @api public
      # @since 2.0.0
      def app
        Hanami.app
      end

      # Returns true if the slice is Hanami.app
      #
      # @return [Boolean]
      #
      # @api public
      # @since 2.2.0
      def app?
        eql?(app)
      end

      # Returns the slice's config.
      #
      # A slice's config is copied from the app config at time of first access.
      #
      # @return [Hanami::Config]
      #
      # @see App::ClassMethods.config
      #
      # @api public
      # @since 2.0.0
      def config
        @config ||= app.config.dup.tap do |slice_config|
          # Unset config from app that does not apply to ordinary slices
          slice_config.root = nil
        end
      end

      # Evaluates the block for a given app environment only.
      #
      # If the given `env_name` matches {Hanami.env}, then the block will be evaluated in the
      # context of `self` (the slice) via `instance_eval`. The slice is also passed as the block's
      # optional argument.
      #
      # If the env does not match, then the block is not evaluated at all.
      #
      # @example
      #   module MySlice
      #     class Slice < Hanami::Slice
      #       environment(:test) do
      #         config.logger.level = :info
      #       end
      #     end
      #   end
      #
      # @overload environment(env_name)
      #   @param env_name [Symbol] the environment name
      #
      # @overload environment(env_name)
      #   @param env_name [Symbol] the environment name
      #   @yieldparam slice [self] the slice
      #
      # @return [self]
      #
      # @see Hanami.env
      #
      # @api public
      # @since 2.0.0
      def environment(env_name, &block)
        instance_eval(&block) if env_name == config.env
        self
      end

      # Returns a {SliceName} for the slice, an object with methods returning the name of the slice
      # in various formats.
      #
      # @return [SliceName]
      #
      # @api public
      # @since 2.0.0
      def slice_name
        @slice_name ||= SliceName.new(self, inflector: method(:inflector))
      end

      # Returns the constant for the slice's module namespace.
      #
      # @example
      #   MySlice::Slice.namespace # => MySlice
      #
      # @return [Module] the namespace module constant
      #
      # @see SliceName#namespace
      #
      # @api public
      # @since 2.0.0
      def namespace
        slice_name.namespace
      end

      # Returns the slice's root, either the root as explicitly configured, or a default fallback of
      # the slice's name within the app's `slices/` dir.
      #
      # @return [Pathname]
      #
      # @see Config#root
      #
      # @api public
      # @since 2.0.0
      def root
        # Provides a best guess for a root when it is not yet configured.
        #
        # This is particularly useful for user-defined slice classes that access `settings` inside
        # the class body (since the root needed to find the settings file). In this case,
        # `configuration.root` may be nil when `settings` is called, since the root is configured by
        # `SliceRegistrar#configure_slice` _after_ the class is loaded.
        #
        # In common cases, this best guess will be correct since most Hanami slices will be expected
        # to live in the app SLICES_DIR. For advanced cases, the correct slice root should be
        # explicitly configured at the beginning of the slice class body, before any calls to
        # `settings`.
        config.root || app.root.join(SLICES_DIR, slice_name.to_s)
      end

      # Returns the slice's root component directory, accounting for App as a special case.
      #
      # @return [Pathname]
      #
      # @api public
      # @since 2.2.0
      def source_path
        app? ? root.join(APP_DIR) : root
      end

      # Returns the slice's root component directory, as a path relative to the app's root.
      #
      # @return [Pathname]
      #
      # @see #source_path
      #
      # @api public
      # @since 2.3.0
      def relative_source_path
        source_path.relative_path_from(app.root)
      end

      # Returns the slice's configured inflector.
      #
      # Unless explicitly re-configured for the slice, this will be the app's inflector.
      #
      # @return [Dry::Inflector]
      #
      # @see Config#inflector
      # @see Config#inflections
      #
      # @api public
      # @since 2.0.0
      def inflector
        config.inflector
      end

      # @overload prepare
      #   Prepares the slice.
      #
      #   This will define the slice's `Slice` and `Deps` constants, make all Ruby source files
      #   inside the slice's root dir autoloadable, as well as lazily loadable as container
      #   components.
      #
      #   Call `prepare` when you want to access particular components within the slice while still
      #   minimizing load time. Preparing slices is the approach taken when loading the Hanami
      #   console or when running tests.
      #
      #   @return [self]
      #
      #   @see #boot
      #
      #   @api public
      #   @since 2.0.0
      #
      # @overload prepare(provider_name)
      #   Prepares a provider.
      #
      #   This triggers the provider's `prepare` lifecycle step.
      #
      #   @param provider_name [Symbol] the name of the provider to start
      #
      #   @return [self]
      #
      #   @api public
      #   @since 2.0.0
      def prepare(provider_name = nil)
        if provider_name
          container.prepare(provider_name)
        else
          prepare_slice
        end

        self
      end

      # Captures the given block to be called with the slice's container during the slice's
      # `prepare` step, after the slice has already configured the container.
      #
      # This is intended for advanced usage only and should not be needed for ordinary slice
      # configuration and usage.
      #
      # @example
      #   module MySlice
      #     class Slice < Hanami::Slice
      #       prepare_container do |container|
      #         # ...
      #       end
      #     end
      #   end
      #
      # @yieldparam container [Dry::System::Container] the slice's container
      #
      # @return [self]
      #
      # @see #prepare
      #
      # @api public
      # @since 2.0.0
      def prepare_container(&block)
        @prepare_container_block = block
        self
      end

      # Boots the slice.
      #
      # This will prepare the slice (if not already prepared), start each of its providers, register
      # all the slice's components from its Ruby source files, and import components from any other
      # slices. It will also boot any of the slice's own registered nested slices. It will then
      # freeze its container so no further components can be registered.
      #
      # Call `boot` if you want to fully load a slice and incur all load time up front, such as when
      # preparing an app to serve web requests. Booting slices is the approach taken when running
      # Hanami's standard Puma setup (see `config.ru`).
      #
      # @return [self]
      #
      # @see #prepare
      #
      # @api public
      # @since 2.0.0
      def boot
        return self if booted?

        prepare

        container.finalize!
        slices.each(&:boot)

        @booted = true

        self
      end

      # Shuts down the slice's providers, as well as the providers in any nested slices.
      #
      # @return [self]
      #
      # @api public
      # @since 2.0.0
      def shutdown
        slices.each(&:shutdown)
        container.shutdown!
        self
      end

      # Returns true if the slice has been prepared.
      #
      # @return [Boolean]
      #
      # @see #prepare
      #
      # @api public
      # @since 2.0.0
      def prepared?
        !!@prepared
      end

      # Returns true if the slice has been booted.
      #
      # @return [Boolean]
      #
      # @see #boot
      #
      # @api public
      # @since 2.0.0
      def booted?
        !!@booted
      end

      # Returns the slice's collection of nested slices.
      #
      # @return [SliceRegistrar]
      #
      # @see #register_slice
      #
      # @api public
      # @since 2.0.0
      def slices
        @slices ||= SliceRegistrar.new(self)
      end

      # @overload register_slice(name, &block)
      #   Registers a nested slice with the given name.
      #
      #   This will define a new {Slice} subclass for the slice. If a block is given, it is passed
      #   the class object, and will be evaluated in the context of the class like `class_eval`.
      #
      #   @example
      #     MySlice::Slice.register_slice do
      #       # Configure the slice or do other class-level things here
      #     end
      #
      #   @param name [Symbol] the identifier for the slice to be registered
      #   @yieldparam slice [Hanami::Slice] the newly defined slice class
      #
      # @overload register_slice(name, slice_class)
      #   Registers a nested slice with the given name.
      #
      #   The given `slice_class` will be registered as the slice. It must be a subclass of {Slice}.
      #
      #   @param name [Symbol] the identifier for the slice to be registered
      #   @param slice_class [Hanami::Slice]
      #
      # @return [slices]
      #
      # @see SliceRegistrar#register
      #
      # @api public
      # @since 2.0.0
      def register_slice(...)
        slices.register(...)
      end

      # Registers a component in the slice's container.
      #
      # @overload register(key, object)
      #   Registers the given object as the component. This same object will be returned whenever
      #   the component is resolved.
      #
      #   @param key [String] the component's key
      #   @param object [Object] the object to register as the component
      #
      # @overload register(key, memoize: false, &block)
      #   Registers the given block as the component. When the component is resolved, the return
      #   value of the block will be returned.
      #
      #   Since the block is not called until resolution-time, this is a useful way to register
      #   components that have dependencies on other components in the container, which as yet may
      #   be unavailable at the time of registration.
      #
      #   All auto-registered components are registered in block form.
      #
      #   When `memoize` is true, the component will be memoized upon first resolution and the same
      #   object returned on all subsequent resolutions, meaning the block is only called once.
      #   Otherwise, the block will be called and a new object returned on every resolution.
      #
      #   @param key [String] the component's key
      #   @param memoize [Boolean]
      #   @yieldreturn [Object] the object to register as the component
      #
      # @overload register(key, call: true, &block)
      #   Registers the given block as the component. When `call: false` is given, then the block
      #   itself will become the component.
      #
      #   When such a component is resolved, the block will not be called, and instead the `Proc`
      #   object for that block will be returned.
      #
      #   @param key [String] the component's key
      #   @param call [Boolean]
      #
      # @return [container]
      #
      # @see #[]
      # @see #resolve
      #
      # @api public
      # @since 2.0.0
      def register(...)
        container.register(...)
      end

      # @overload register_provider(name, namespace: nil, from: nil, source: nil, if: true, &block)
      #   Registers a provider and its lifecycle hooks.
      #
      #   In most cases, you should call this from a dedicated file for the provider in your app or
      #   slice's `config/providers/` dir. This allows the provider to be loaded when individual
      #   matching components are resolved (for prepared slices) or when slices are booted.
      #
      #   @example Simple provider
      #     # config/providers/db.rb
      #     Hanami.app.register_provider(:db) do
      #       start do
      #         require "db"
      #         register("db", DB.new)
      #       end
      #     end
      #
      #   @example Provider with lifecycle steps, also using dependencies from the target container
      #     # config/providers/db.rb
      #     Hanami.app.register_provider(:db) do
      #       prepare do
      #         require "db"
      #         db = DB.new(target_container["settings"].database_url)
      #         register("db", db)
      #       end
      #
      #       start do
      #         container["db"].establish_connection
      #       end
      #
      #       stop do
      #         container["db"].close_connection
      #       end
      #     end
      #
      #   @example Probvider registration under a namespace
      #     # config/providers/db.rb
      #     Hanami.app.register_provider(:persistence, namespace: true) do
      #       start do
      #         require "db"
      #
      #         # Namespace option above means this will be registered as "persistence.db"
      #         register("db", DB.new)
      #       end
      #     end
      #
      #   @param name [Symbol] the unique name for the provider
      #   @param namespace [Boolean, String, nil] register components from the provider with given
      #     namespace. May be an explicit string, or `true` for the namespace to be the provider's
      #     name
      #   @param from [Symbol, nil] the group for an external provider source to use, with the
      #     provider source name inferred from `name` or passed explicitly as `source:`
      #   @param source [Symbol, nil] the name of the external provider source to use, if different
      #     from the value provided as `name`
      #   @param if [Boolean] a boolean-returning expression to determine whether to register the
      #     provider
      #
      #   @return [container]
      #
      #   @api public
      #   @since 2.0.0
      def register_provider(...)
        container.register_provider(...)
      end

      # @api public
      # @since 2.1.0
      def configure_provider(*args, **kwargs, &block)
        container.register_provider(*args, **kwargs, from: :hanami, &block)
      end

      # @overload start(provider_name)
      #   Starts a provider.
      #
      #   This triggers the provider's `prepare` and `start` lifecycle steps.
      #
      #   @example
      #     MySlice::Slice.start(:persistence)
      #
      #   @param provider_name [Symbol] the name of the provider to start
      #
      #   @return [container]
      #
      #   @api public
      #   @since 2.0.0
      def start(...)
        container.start(...)
      end

      # @overload stop(provider_name)
      #   Stops a provider.
      #
      #   This triggers the provider's `stop` lifecycle hook.
      #
      #   @example
      #     MySlice::Slice.stop(:persistence)
      #
      #   @param provider_name [Symbol] the name of the provider to start
      #
      #   @return [container]
      #
      #   @api public
      #   @since 2.0.0
      def stop(...)
        container.stop(...)
      end

      # @overload key?(key)
      #   Returns true if the component with the given key is registered in the container.
      #
      #   For a prepared slice, calling `key?` will also try to load the component if not loaded
      #   already.
      #
      #   @param key [String, Symbol] the component key
      #
      #   @return [Boolean]
      #
      #   @api public
      #   @since 2.0.0
      def key?(...)
        container.key?(...)
      end

      # Required for the slice to act as a provider target
      # @api public
      # @since 2.2.0
      def registered?(...)
        container.registered?(...)
      end

      # Returns an array of keys for all currently registered components in the container.
      #
      # For a prepared slice, this will be the set of components that have been previously resolved.
      # For a booted slice, this will be all components available for the slice.
      #
      # @return [Array<String>]
      #
      # @api public
      # @since 2.0.0
      def keys
        container.keys
      end

      # @overload [](key)
      #   Resolves the component with the given key from the container.
      #
      #   For a prepared slice, this will attempt to load and register the matching component if it
      #   is not loaded already. For a booted slice, this will return from already registered
      #   components only.
      #
      #   @return [Object] the resolved component's object
      #
      #   @raise Dry::Container::KeyError if the component could not be found or loaded
      #
      #   @see #resolve
      #
      #   @api public
      #   @since 2.0.0
      def [](...)
        container.[](...)
      end

      # @see #[]
      #
      # @api public
      # @since 2.0.0
      def resolve(...)
        container.resolve(...)
      end

      # Specifies the components to export from the slice.
      #
      # Slices importing from this slice can import the specified components only.
      #
      # @example
      #   module MySlice
      #     class Slice < Hanami::Slice
      #       export ["search", "index_entity"]
      #     end
      #   end
      #
      # @param keys [Array<String>] the component keys to export
      #
      # @return [self]
      #
      # @api public
      # @since 2.0.0
      def export(keys)
        container.config.exports = keys
        self
      end

      # @overload import(from:, as: nil, keys: nil)
      #   Specifies components to import from another slice.
      #
      #   Booting a slice will register all imported components. For a prepared slice, these
      #   components will be be imported automatically when resolved.
      #
      #   @example
      #     module MySlice
      #       class Slice < Hanami:Slice
      #         # Component from Search::Slice will import as "search.index_entity"
      #         import keys: ["index_entity"], from: :search
      #       end
      #     end
      #
      #   @example Other import variations
      #     # Different key namespace: component will be "search_backend.index_entity"
      #     import keys: ["index_entity"], from: :search, as: "search_backend"
      #
      #     # Import to root key namespace: component will be "index_entity"
      #     import keys: ["index_entity"], from: :search, as: nil
      #
      #     # Import all components
      #     import from: :search
      #
      #   @param keys [Array<String>, nil] Array of component keys to import. To import all
      #     available components, omit this argument.
      #   @param from [Symbol] name of the slice to import from
      #   @param as [Symbol, String, nil]
      #
      #   @see #export
      #
      #   @api public
      #   @since 2.0.0
      def import(from:, **kwargs)
        slice = self

        container.after(:configure) do
          if from.is_a?(Symbol) || from.is_a?(String)
            slice_name = from
            from = slice.parent.slices[from.to_sym].container
          end

          as = kwargs[:as] || slice_name

          import(from: from, as: as, **kwargs)
        end
      end

      # Returns the slice's settings, or nil if no settings are defined.
      #
      # You can define your settings in `config/settings.rb`.
      #
      # @return [Hanami::Settings, nil]
      #
      # @see Hanami::Settings
      #
      # @api public
      # @since 2.0.0
      def settings
        return @settings if instance_variable_defined?(:@settings)

        @settings = Settings.load_for_slice(self)
      end

      # Returns the slice's routes, or nil if no routes are defined.
      #
      # You can define your routes in `config/routes.rb`.
      #
      # @return [Hanami::Routes, nil]
      #
      # @see Hanami::Routes
      #
      # @api public
      # @since 2.0.0
      def routes
        return @routes if instance_variable_defined?(:@routes)

        @routes = load_routes
      end

      # Returns the slice's router, if or nil if no routes are defined.
      #
      # An optional `inspector`, implementing the `Hanami::Router::Inspector` interface, may be
      # provided at first call (the router is then memoized for subsequent accesses). An inspector
      # is used by the `hanami routes` CLI comment to provide a list of available routes.
      #
      # The returned router is a {Slice::Router}, which provides all `Hanami::Router` functionality,
      # with the addition of support for slice mounting with the {Slice::Router#slice}.
      #
      # @param inspector [Hanami::Router::Inspector, nil] an optional routes inspector
      #
      # @return [Hanami::Slice::Router, nil]
      #
      # @api public
      # @since 2.0.0
      def router(inspector: nil)
        raise SliceLoadError, "#{self} must be prepared before loading the router" unless prepared?

        @_mutex.synchronize do
          @_router ||= load_router(inspector: inspector)
        end
      end

      # Returns a [Rack][rack] app for the slice, or nil if no routes are defined.
      #
      # The rack app will be memoized on first access.
      #
      # [rack]: https://github.com/rack/rack
      #
      # @return [#call, nil] the rack app, or nil if no routes are defined
      #
      # @see #routes
      # @see #router
      #
      # @api public
      # @since 2.0.0
      def rack_app
        return unless router

        @rack_app ||= router.to_rack_app
      end

      # @overload call(rack_env)
      #   Calls the slice's [Rack][rack] app and returns a Rack-compatible response object
      #
      #   [rack]: https://github.com/rack/rack
      #
      #   @param rack_env [Hash] the Rack environment for the request
      #
      #   @return [Array] the three-element Rack response array
      #
      #   @see #rack_app
      #
      #   @api public
      #   @since 2.0.0
      def call(...)
        rack_app.call(...)
      end

      private

      # rubocop:disable Metrics/AbcSize

      def prepare_slice
        return self if prepared?

        config.finalize!

        ensure_slice_name
        ensure_slice_consts
        ensure_root

        prepare_all

        instance_exec(container, &@prepare_container_block) if @prepare_container_block
        container.configured!

        prepare_autoloader

        # Load child slices last, ensuring their parent is fully prepared beforehand
        # (useful e.g. for slices that may wish to access constants defined in the
        # parent's autoloaded directories)
        prepare_slices

        @prepared = true

        self
      end

      def ensure_slice_name
        unless name
          raise SliceLoadError, "Slice must have a class name before it can be prepared"
        end
      end

      def ensure_slice_consts
        if namespace.const_defined?(:Container) || namespace.const_defined?(:Deps)
          raise(
            SliceLoadError,
            "#{namespace}::Container and #{namespace}::Deps constants must not already be defined"
          )
        end
      end

      def ensure_root
        unless config.root
          raise SliceLoadError, "Slice must have a `config.root` before it can be prepared"
        end
      end

      def prepare_all
        prepare_settings
        prepare_container_consts
        prepare_container_plugins
        prepare_container_base_config
        prepare_container_component_dirs
        prepare_container_imports
        prepare_container_providers
      end

      def prepare_settings
        container.register(:settings, settings) if settings
      end

      def prepare_container_consts
        namespace.const_set :Container, container
        namespace.const_set :Deps, container.injector
      end

      def prepare_container_plugins
        container.use(:env, inferrer: -> { Hanami.env })

        container.use(
          :zeitwerk,
          loader: autoloader,
          run_setup: false,
          eager_load: false
        )
      end

      def prepare_container_base_config
        container.config.name = slice_name.to_sym
        container.config.root = root
        container.config.provider_registrar = ProviderRegistrar.for_slice(self)
        container.config.provider_dirs = [File.join("config", "providers")]
        container.config.registrations_dir = File.join("config", "registrations")

        container.config.env = config.env
        container.config.inflector = config.inflector
      end

      def prepare_container_component_dirs
        return unless root.directory?

        # Component files in both the root and `lib/` define classes in the slice's
        # namespace

        if root.join(LIB_DIR)&.directory?
          container.config.component_dirs.add(LIB_DIR) do |dir|
            dir.namespaces.add_root(key: nil, const: slice_name.name)
          end
        end

        # When auto-registering components in the root, ignore files in `config/` (this is
        # for framework config only), `lib/` (these will be auto-registered as above), as
        # well as the configured no_auto_register_paths
        no_auto_register_paths = ([LIB_DIR, CONFIG_DIR] + config.no_auto_register_paths)
          .map { |path|
            path.end_with?(File::SEPARATOR) ? path : "#{path}#{File::SEPARATOR}"
          }

        # TODO: Change `""` (signifying the root) once dry-rb/dry-system#238 is resolved
        container.config.component_dirs.add("") do |dir|
          dir.namespaces.add_root(key: nil, const: slice_name.name)
          dir.auto_register = -> component {
            relative_path = component.file_path.relative_path_from(root).to_s
            !relative_path.start_with?(*no_auto_register_paths)
          }
        end
      end

      def prepare_container_imports
        import(
          keys: config.shared_app_component_keys,
          from: app.container,
          as: nil
        )
      end

      # rubocop:disable Metrics/PerceivedComplexity
      def prepare_container_providers
        # Check here for the `routes` definition only, not `router` itself, because the
        # `router` requires the slice to be prepared before it can be loaded, and at this
        # point we're still in the process of preparing.
        if routes?
          require_relative "providers/routes"
          register_provider(:routes, source: Providers::Routes)
        end

        if assets_dir? && Hanami.bundled?("hanami-assets")
          require_relative "providers/assets"
          register_provider(:assets, source: Providers::Assets)
        end

        if Hanami.bundled?("hanami-db")
          # Explicit require here to ensure the provider source registers itself, to allow the user
          # to configure it within their own concrete provider file.
          require_relative "providers/db"

          if register_db_provider?
            # Only register providers if the user hasn't provided their own
            unless container.providers[:db]
              register_provider(:db, namespace: true, source: Providers::DB)
            end

            unless container.providers[:relations]
              register_provider(:relations, namespace: true, source: Providers::Relations)
            end
          end
        end

        if Hanami.bundled?("i18n")
          require_relative "providers/i18n"

          if i18n_config_dir? && !container.providers[:i18n]
            register_provider(:i18n, source: Providers::I18n)
          end
        end
      end
      # rubocop:enable Metrics/PerceivedComplexity

      def prepare_autoloader
        autoloader.tag = "hanami.slices.#{slice_name}"

        # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
        # This method adds other dirs that are not otherwise configured as component dirs.

        # Everything in the slice root can be autoloaded except `config/` and `slices/`,
        # which are framework-managed directories

        if root.join(CONFIG_DIR)&.directory?
          autoloader.ignore(root.join(CONFIG_DIR))
        end

        if root.join(SLICES_DIR)&.directory?
          autoloader.ignore(root.join(SLICES_DIR))
        end

        autoloader.setup
      end

      def prepare_slices
        slices.load_slices.each(&:prepare)
        slices.freeze
      end

      def routes?
        return false unless Hanami.bundled?("hanami-router")

        return true if namespace.const_defined?(ROUTES_CLASS_NAME)

        root.join("#{ROUTES_PATH}#{RB_EXT}").file?
      end

      def load_routes
        return false unless Hanami.bundled?("hanami-router")

        if root.directory?
          routes_require_path = root.join(ROUTES_PATH).to_s

          begin
            require_relative "./routes"
            require routes_require_path
          rescue LoadError => exception
            raise exception unless exception.path == routes_require_path
          end
        end

        begin
          routes_class = namespace.const_get(ROUTES_CLASS_NAME)
          routes_class.routes
        rescue NameError => exception
          raise exception unless exception.name == ROUTES_CLASS_NAME.to_sym
        end
      end

      # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
      def load_router(inspector:)
        return unless routes

        require_relative "slice/router"

        slice = self
        config = self.config
        rack_monitor = self["rack.monitor"]

        show_welcome = Hanami.env?(:development) && routes.empty?
        render_errors = render_errors?
        render_detailed_errors = render_detailed_errors?

        error_handlers = {}.tap do |hsh|
          if render_errors || render_detailed_errors
            hsh[:not_allowed] = ROUTER_NOT_ALLOWED_HANDLER
            hsh[:not_found] = ROUTER_NOT_FOUND_HANDLER
          end
        end

        Slice::Router.new(
          inspector: inspector,
          inflector: inflector,
          routes: routes,
          resolver: config.router.resolver.new(slice: self),
          **error_handlers,
          **config.router.options
        ) do
          use(rack_monitor)

          use(Hanami::Web::Welcome) if show_welcome

          use(
            Hanami::Middleware::RenderErrors,
            config,
            Hanami::Middleware::PublicErrorsApp.new(slice.root.join("public"))
          )

          if render_detailed_errors
            require "hanami/webconsole"
            use(Hanami::Webconsole::Middleware, config)
          end

          if Hanami.bundled?("hanami-action")
            if config.actions.method_override
              require "rack/method_override"
              use(Rack::MethodOverride)
            end

            if config.actions.sessions.enabled?
              use(*config.actions.sessions.middleware)
            end

            if config.actions.content_security_policy && # rubocop:disable Style/SafeNavigation
               config.actions.content_security_policy.nonce?
              use(*config.actions.content_security_policy.middleware)
            end
          end

          if Hanami.bundled?("hanami-assets") && config.assets.serve
            use(Hanami::Middleware::Assets)
          end

          middleware_stack.update(config.middleware_stack)
        end
      end
      # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

      def render_errors?
        config.render_errors
      end

      def render_detailed_errors?
        config.render_detailed_errors && Hanami.bundled?("hanami-webconsole")
      end

      ROUTER_NOT_ALLOWED_HANDLER = -> env, allowed_http_methods {
        raise Hanami::Router::NotAllowedError.new(env, allowed_http_methods)
      }.freeze
      private_constant :ROUTER_NOT_ALLOWED_HANDLER

      ROUTER_NOT_FOUND_HANDLER = -> env {
        raise Hanami::Router::NotFoundError.new(env)
      }.freeze
      private_constant :ROUTER_NOT_FOUND_HANDLER

      def assets_dir?
        source_path.join("assets").directory?
      end

      def i18n_config_dir?
        root.join("config", "i18n").directory?
      end

      def register_db_provider?
        concrete_db_provider? ||
          db_config_dir? ||
          relations_dir? ||
          db_source_dir? ||
          import_db_from_parent?
      end

      def concrete_db_provider?
        root.join(CONFIG_DIR, "providers", "db.rb").exist?
      end

      def db_config_dir?
        root.join("config", "db").directory?
      end

      def relations_dir?
        source_path.join("relations").directory?
      end

      def db_source_dir?
        source_path.join("db").directory?
      end

      def import_db_from_parent?
        parent &&
          config.db.import_from_parent &&
          parent.container.providers[:db]
      end

      # rubocop:enable Metrics/AbcSize
    end
  end
end


def import(from:, **kwargs)
        slice = self

        container.after(:configure) do
          if from.is_a?(Symbol) || from.is_a?(String)
            slice_name = from
            from = slice.parent.slices[from.to_sym].container
          end

          as = kwargs[:as] || slice_name

          import(from: from, as: as, **kwargs)
        end
      end


def prepare_slice
        return self if prepared?

        config.finalize!

        ensure_slice_name
        ensure_slice_consts
        ensure_root

        prepare_all

        instance_exec(container, &@prepare_container_block) if @prepare_container_block
        container.configured!

        prepare_autoloader

        # Load child slices last, ensuring their parent is fully prepared beforehand
        # (useful e.g. for slices that may wish to access constants defined in the
        # parent's autoloaded directories)
        prepare_slices

        @prepared = true

        self
      end


def prepare_container_component_dirs
        return unless root.directory?

        # Component files in both the root and `lib/` define classes in the slice's
        # namespace

        if root.join(LIB_DIR)&.directory?
          container.config.component_dirs.add(LIB_DIR) do |dir|
            dir.namespaces.add_root(key: nil, const: slice_name.name)
          end
        end

        # When auto-registering components in the root, ignore files in `config/` (this is
        # for framework config only), `lib/` (these will be auto-registered as above), as
        # well as the configured no_auto_register_paths
        no_auto_register_paths = ([LIB_DIR, CONFIG_DIR] + config.no_auto_register_paths)
          .map { |path|
            path.end_with?(File::SEPARATOR) ? path : "#{path}#{File::SEPARATOR}"
          }

        # TODO: Change `""` (signifying the root) once dry-rb/dry-system#238 is resolved
        container.config.component_dirs.add("") do |dir|
          dir.namespaces.add_root(key: nil, const: slice_name.name)
          dir.auto_register = -> component {
            relative_path = component.file_path.relative_path_from(root).to_s
            !relative_path.start_with?(*no_auto_register_paths)
          }
        end
      end


def prepare_container_providers
        # Check here for the `routes` definition only, not `router` itself, because the
        # `router` requires the slice to be prepared before it can be loaded, and at this
        # point we're still in the process of preparing.
        if routes?
          require_relative "providers/routes"
          register_provider(:routes, source: Providers::Routes)
        end

        if assets_dir? && Hanami.bundled?("hanami-assets")
          require_relative "providers/assets"
          register_provider(:assets, source: Providers::Assets)
        end

        if Hanami.bundled?("hanami-db")
          # Explicit require here to ensure the provider source registers itself, to allow the user
          # to configure it within their own concrete provider file.
          require_relative "providers/db"

          if register_db_provider?
            # Only register providers if the user hasn't provided their own
            unless container.providers[:db]
              register_provider(:db, namespace: true, source: Providers::DB)
            end

            unless container.providers[:relations]
              register_provider(:relations, namespace: true, source: Providers::Relations)
            end
          end
        end

        if Hanami.bundled?("i18n")
          require_relative "providers/i18n"

          if i18n_config_dir? && !container.providers[:i18n]
            register_provider(:i18n, source: Providers::I18n)
          end
        end
      end


def prepare_autoloader
        autoloader.tag = "hanami.slices.#{slice_name}"

        # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
        # This method adds other dirs that are not otherwise configured as component dirs.

        # Everything in the slice root can be autoloaded except `config/` and `slices/`,
        # which are framework-managed directories

        if root.join(CONFIG_DIR)&.directory?
          autoloader.ignore(root.join(CONFIG_DIR))
        end

        if root.join(SLICES_DIR)&.directory?
          autoloader.ignore(root.join(SLICES_DIR))
        end

        autoloader.setup
      end


def load_routes
        return false unless Hanami.bundled?("hanami-router")

        if root.directory?
          routes_require_path = root.join(ROUTES_PATH).to_s

          begin
            require_relative "./routes"
            require routes_require_path
          rescue LoadError => exception
            raise exception unless exception.path == routes_require_path
          end
        end

        begin
          routes_class = namespace.const_get(ROUTES_CLASS_NAME)
          routes_class.routes
        rescue NameError => exception
          raise exception unless exception.name == ROUTES_CLASS_NAME.to_sym
        end
      end


def load_router(inspector:)
        return unless routes

        require_relative "slice/router"

        slice = self
        config = self.config
        rack_monitor = self["rack.monitor"]

        show_welcome = Hanami.env?(:development) && routes.empty?
        render_errors = render_errors?
        render_detailed_errors = render_detailed_errors?

        error_handlers = {}.tap do |hsh|
          if render_errors || render_detailed_errors
            hsh[:not_allowed] = ROUTER_NOT_ALLOWED_HANDLER
            hsh[:not_found] = ROUTER_NOT_FOUND_HANDLER
          end
        end

        Slice::Router.new(
          inspector: inspector,
          inflector: inflector,
          routes: routes,
          resolver: config.router.resolver.new(slice: self),
          **error_handlers,
          **config.router.options
        ) do
          use(rack_monitor)

          use(Hanami::Web::Welcome) if show_welcome

          use(
            Hanami::Middleware::RenderErrors,
            config,
            Hanami::Middleware::PublicErrorsApp.new(slice.root.join("public"))
          )

          if render_detailed_errors
            require "hanami/webconsole"
            use(Hanami::Webconsole::Middleware, config)
          end

          if Hanami.bundled?("hanami-action")
            if config.actions.method_override
              require "rack/method_override"
              use(Rack::MethodOverride)
            end

            if config.actions.sessions.enabled?
              use(*config.actions.sessions.middleware)
            end

            if config.actions.content_security_policy && # rubocop:disable Style/SafeNavigation
               config.actions.content_security_policy.nonce?
              use(*config.actions.content_security_policy.middleware)
            end
          end

          if Hanami.bundled?("hanami-assets") && config.assets.serve
            use(Hanami::Middleware::Assets)
          end

          middleware_stack.update(config.middleware_stack)
        end
      end


# frozen_string_literal: true

module Hanami
  module Env
    # @since 2.0.1
    # @api private
    @_loaded = false

    # Uses [dotenv](https://github.com/bkeepers/dotenv) (if available) to populate `ENV` from
    # various `.env` files.
    #
    # For a given `HANAMI_ENV` environment, the `.env` files are looked up in the following order:
    #
    # - .env.{environment}.local
    # - .env.local (unless the environment is `test`)
    # - .env.{environment}
    # - .env
    #
    # If dotenv is unavailable, the method exits and does nothing.
    #
    # @since 2.0.1
    # @api private
    def self.load(env = Hanami.env)
      return unless Hanami.bundled?("dotenv")
      return if loaded?

      dotenv_files = [
        ".env.#{env}.local",
        (".env.local" unless env == :test),
        ".env.#{env}",
        ".env"
      ].compact

      require "dotenv"
      Dotenv.load(*dotenv_files)

      loaded!
    end

    # @since 2.0.1
    # @api private
    def self.loaded?
      @_loaded
    end

    # @since 2.0.1
    # @api private
    def self.loaded!
      @_loaded = true
    end
  end
end


# frozen_string_literal: true

require "dry/configurable"
require "dry/logger"

module Hanami
  class Config
    # Hanami logger config
    #
    # @api public
    # @since 2.0.0
    class Logger
      include Dry::Configurable

      # @return [Hanami::SliceName]
      #
      # @api private
      # @since 2.0.0
      attr_reader :app_name

      # @return [Symbol]
      #
      # @api private
      # @since 2.0.0
      attr_reader :env

      # @!attribute [rw] level
      #   Sets or returns the logger's level.
      #
      #   Defaults to `:info` for the production environment and `:debug` for all others.
      #   Can be set via the `HANAMI_LOG_LEVEL` environment variable.
      #
      #   @return [Symbol]
      #
      #   @api public
      #   @since 2.0.0
      setting :level

      # @!attribute [rw] stream
      #   Sets or returns the logger's stream.
      #
      #   This can be a file path or an `IO`-like object for the logger to write to.
      #
      #   Defaults to `"log/test.log"` for the test environment and `$stdout` for all others.
      #
      #   @return [String, #write]
      #
      #   @api public
      #   @since 2.0.0
      setting :stream, default: $stdout

      # @!attribute [rw] formatter
      #   Sets or returns the logger's formatter.
      #
      #   This may be a name that matches a formatter registered with `Dry::Logger`, which includes
      #   `:string`, `:rack` and `:json`.
      #
      #   This may also be an instance of Ruby's built-in `::Logger::Formatter` or any compatible
      #   object.
      #
      #   Defaults to `:json` for the production environment, and `:rack` for all others.
      #
      #   @return [Symbol, ::Logger::Formatter]
      #
      #   @api public
      #   @since 2.0.0
      setting :formatter, default: :string

      # @!attribute [rw] template
      #   Sets or returns log entry string template
      #
      #   Defaults to `false`.
      #
      #   @return [Boolean]
      #
      #   @api public
      #   @since 2.0.0
      setting :template, default: :details

      # @!attribute [rw] filters
      #   Sets or returns an array of attribute names to filter from logs.
      #
      #   Defaults to `["_csrf", "password", "password_confirmation"]`. If you want to preserve
      #   these defaults, append to this array rather than reassigning it.
      #
      #   @return [Array<String>]
      #
      #   @api public
      #   @since 2.0.0
      setting :filters, default: %w[_csrf password password_confirmation].freeze

      # @!attribute [rw] logger_constructor
      #   Sets or returns the constructor proc to use for the logger instantiation.
      #
      #   Defaults to either `Config#production_logger` or `Config#development_logger`
      #
      #   @api public
      #   @since 2.0.0
      setting :logger_constructor

      # @!attribute [rw] options
      #   Sets or returns a hash of options to pass to the {logger_constructor} when initializing
      #   the logger.
      #
      #   Defaults to `{}`
      #
      #   @return [Hash]
      #
      #   @api public
      #   @since 2.0.0
      setting :options, default: {}

      # Returns a new `Logger` config.
      #
      # You should not need to initialize this directly, instead use {Hanami::Config#logger}.
      #
      # @param env [Symbol] the Hanami env
      # @param app_name [Hanami::SliceName]
      #
      # @api private
      def initialize(env:, app_name:)
        @app_name = app_name
        @env = env

        case env
        when :development
          config.level = :debug
          config.options = {colorize: true}
          config.logger_constructor = method(:development_logger)
        when :test
          config.level = :debug
          config.stream = File.join("log", "#{env}.log")
          config.logger_constructor = method(:development_logger)
        else
          config.level = :info
          config.formatter = :json
          config.logger_constructor = method(:production_logger)
        end
      end

      # Returns a new instance of the logger.
      #
      # @return [logger_class]
      #
      # @api public
      # @since 2.0.0
      def instance
        logger_constructor.call(env, app_name.name, **logger_constructor_options)
      end

      # Build an instance of a development logger
      #
      # This logger is used in both development and test
      #
      # @return [Dry::Logger::Dispatcher]
      # @since 2.0.0
      # @api private
      def development_logger(_env, app_name, **options)
        Dry.Logger(app_name, **options) do |setup|
          setup
            .add_backend(log_if: -> entry { !entry.tag?(:rack) && !entry.tag?(:sql) })
            .add_backend(log_if: -> entry { entry.tag?(:rack) }, formatter: Hanami::Logger::RackFormatter)
            .add_backend(log_if: -> entry { entry.tag?(:sql) }, formatter: Hanami::Logger::SQLFormatter)
        end
      end

      # Build an instance of a production logger
      #
      # This logger is used in both development and test
      #
      # @return [Dry::Logger::Dispatcher]
      # @since 2.0.0
      # @api private
      def production_logger(_env, app_name, **options)
        Dry.Logger(app_name, **options)
      end

      private

      # @api private
      def logger_constructor_options
        {
          stream: stream,
          level: ENV["HANAMI_LOG_LEVEL"]&.to_sym || level,
          formatter: formatter,
          filters: filters,
          template: template,
          **options
        }
      end

      # @api private
      def method_missing(name, *args, &block)
        if config.respond_to?(name)
          config.public_send(name, *args, &block)
        else
          super
        end
      end

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


# frozen_string_literal: true

require "dry/configurable"
require "hanami/view"

module Hanami
  class Config
    # Hanami views config
    #
    # This exposes all the settings from the standalone `Hanami::View` class, pre-configured with
    # sensible defaults for actions within a full Hanami app. It also provides additional settings
    # for further integration of views with other full stack app components.
    #
    # @since 2.1.0
    # @api public
    class Views
      include Dry::Configurable

      # @!attribute [rw] default_template_engine
      #   Sets or returns the default template engine to use for generated views.
      #
      #   Used by generators when no explicit template engine is specified.
      #   Supports `erb`, `haml`, and `slim`.
      #
      #   Defaults to `"erb"`.
      #
      # @example
      #   config.views.default_template_engine = "slim"
      #
      # @return [String] the template engine name
      #
      # @api public
      # @since 2.4.0
      setting :default_template_engine, default: "erb"

      # @api private
      # @since 2.1.0
      attr_reader :base_config
      protected :base_config

      # @api private
      # @since 2.1.0
      def initialize(*)
        super

        @base_config = Hanami::View.config.dup

        configure_defaults
      end

      # @api private
      # @since 2.1.0
      def initialize_copy(source)
        super
        @base_config = source.base_config.dup
      end
      private :initialize_copy

      # @api private
      # @since 2.1.0
      def finalize!
        return self if frozen?

        base_config.finalize!

        super
      end

      private

      def configure_defaults
        self.layout = "app"
      end

      # An inflector for views is not configurable via `config.views.inflector` on an
      # `Hanami::App`. The app-wide inflector is already configurable
      # there as `config.inflector` and will be used as the default inflector for views.
      #
      # A custom inflector may still be provided in an `Hanami::View` subclass, via
      # `config.inflector=`.
      NON_FORWARDABLE_METHODS = %i[inflector inflector=].freeze
      private_constant :NON_FORWARDABLE_METHODS

      def method_missing(name, *args, &block)
        return super if NON_FORWARDABLE_METHODS.include?(name)

        if config.respond_to?(name)
          config.public_send(name, *args, &block)
        elsif base_config.respond_to?(name)
          base_config.public_send(name, *args, &block)
        else
          super
        end
      end

      def respond_to_missing?(name, _include_all = false)
        return false if NON_FORWARDABLE_METHODS.include?(name)

        config.respond_to?(name) || base_config.respond_to?(name) || super
      end
    end
  end
end


# frozen_string_literal: true

require "delegate"
require "json"

module Hanami
  # @api private
  module Web
    # Rack logger for Hanami apps
    #
    # @api private
    # @since 2.0.0
    class RackLogger
      EMPTY_PARAMS = {}.freeze
      private_constant :EMPTY_PARAMS

      REQUEST_METHOD = "REQUEST_METHOD"
      private_constant :REQUEST_METHOD

      HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR"
      private_constant :HTTP_X_FORWARDED_FOR

      REMOTE_ADDR = "REMOTE_ADDR"
      private_constant :REMOTE_ADDR

      SCRIPT_NAME = "SCRIPT_NAME"
      private_constant :SCRIPT_NAME

      PATH_INFO = "PATH_INFO"
      private_constant :PATH_INFO

      ROUTER_PARAMS = "router.params"
      private_constant :ROUTER_PARAMS

      CONTENT_LENGTH = "CONTENT_LENGTH"
      private_constant :CONTENT_LENGTH

      MILLISECOND = "ms"
      private_constant :MILLISECOND

      MICROSECOND = "µs"
      private_constant :MICROSECOND

      # Dynamic extension used in development and test environments
      # @api private
      module Development
        private

        # @since 2.0.0
        # @api private
        def data(env, status:, elapsed:)
          payload = super
          payload.delete(:elapsed_unit)
          payload[:elapsed] = elapsed > 1000 ? "#{elapsed / 1000}ms" : "#{elapsed}#{MICROSECOND}"
          payload
        end
      end

      # @api private
      # @since 2.0.0
      def initialize(logger, env: :development)
        @logger = Hanami::UniversalLogger[logger]
        extend(Development) if %i[development test].include?(env)
      end

      # @api private
      # @since 2.0.0
      def attach(rack_monitor)
        rack_monitor.on :stop do |event|
          log_request event[:env], event[:status], event[:time]
        end

        rack_monitor.on :error do |event|
          # TODO: why we don't provide time on error?
          log_exception event[:env], event[:exception], 500, 0
        end
      end

      # @api private
      # @since 2.0.0
      def log_request(env, status, elapsed)
        logger.tagged(:rack) do
          logger.info do
            data(env, status: status, elapsed: elapsed)
          end
        end
      end

      # @api private
      # @since 2.0.0
      def log_exception(env, exception, status, elapsed)
        logger.tagged(:rack) do
          logger.error(exception) do
            data(env, status: status, elapsed: elapsed)
          end
        end
      end

      private

      attr_reader :logger

      # @api private
      # @since 2.0.0
      def data(env, status:, elapsed:)
        {
          verb: env[REQUEST_METHOD],
          status: status,
          ip: env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR],
          path: "#{env[SCRIPT_NAME]}#{env[PATH_INFO]}",
          length: extract_content_length(env),
          params: env.fetch(ROUTER_PARAMS, EMPTY_PARAMS),
          elapsed: elapsed,
          elapsed_unit: MICROSECOND
        }
      end

      # @api private
      # @since 2.0.0
      def extract_content_length(env)
        value = env[CONTENT_LENGTH]
        !value || value.to_s == "0" ? "-" : value
      end
    end
  end
end


# frozen_string_literal: true

require "dry/logger"

module Hanami
  module Logger
    # SQL query log formatter for Dry Logger.
    #
    # Formats SQL query log entries with a template that mirrors the structure of the built-in rack
    # log formatter, providing consistent visual formatting across both HTTP request and database
    # query logs.
    #
    # For example, in development, SQL logs alongside Rack logs:
    #
    #   [my_app] [INFO] [2026-03-04 10:15:32] SQL sqlite 1.234ms SELECT * FROM users
    #   [my_app] [INFO] [2026-03-04 10:15:32] GET 200 1ms 127.0.0.1 /users -
    #
    # In production, the default JSON formatter handles SQL entries automatically via the structured
    # payload.
    #
    # Supports colorization via Dry Logger's template color tags. When `colorize: true` is set in
    # the logger options (the default in development), the "SQL" label is colorized, and severity
    # is colorized per-level by the parent formatter (e.g. INFO => magenta, ERROR => red).
    #
    # When colorization is enabled and the "rouge" gem is available, SQL queries are syntax
    # highlighted using Rouge's SQL lexer. This is a soft dependency; if Rouge is not installed,
    # queries output as plain, unhighlighted text.
    #
    # The Rouge theme defaults to Gruvbox and can be customized by setting the `HANAMI_SQL_THEME`
    # environment variable to any Rouge theme name (e.g. "github.dark", "monokai", "gruvbox.light").
    # See `Rouge::Theme.registry` for available themes.
    #
    # @see Hanami::Logger::SQLLogger
    #
    # @api private
    class SQLFormatter < Dry::Logger::Formatters::String
      SQL_TEMPLATE = <<~TEXT
        [%<progname>s] [%<severity>s] [%<time>s] SQL %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
      TEXT

      SQL_TEMPLATE_COLORIZED = <<~TEXT
        [%<progname>s] [%<severity>s] [%<time>s] <blue>SQL</blue> %<db>s %<elapsed>s%<elapsed_unit>s %<query>s
      TEXT

      def initialize(**options)
        super
        @template = Dry::Logger::Formatters::Template[
          colorize? ? SQL_TEMPLATE_COLORIZED : SQL_TEMPLATE
        ]
        @sql_colorizer = build_sql_colorizer if colorize?
      end

      private

      def format_query(value)
        if @sql_colorizer
          @sql_colorizer.call(value)
        else
          value
        end
      end

      def build_sql_colorizer
        begin
          require "rouge"
        rescue LoadError
          return nil
        end

        theme_name = ENV.fetch("HANAMI_SQL_THEME", "gruvbox")
        theme_class = Rouge::Theme.find(theme_name) || Rouge::Themes::Gruvbox
        formatter = Rouge::Formatters::Terminal256.new(theme_class.new)

        lexer = Rouge::Lexers::SQL.new

        ->(sql) { formatter.format(lexer.lex(sql)) }
      end
    end
  end
end

class SQLFormatter < < Dry::Logger::Formatters::String


# frozen_string_literal: true

require "rack"

# rubocop:disable Lint/RescueException

module Hanami
  module Middleware
    # Rack middleware that rescues errors raised by the app renders friendly error responses, via a
    # given "errors app".
    #
    # By default, this is enabled only in production mode.
    #
    # @see Hanami::Config#render_errors
    # @see Hanani::Middleware::PublicErrorsApp
    #
    # @api private
    # @since 2.1.0
    class RenderErrors
      # @api private
      # @since 2.1.0
      class RenderableException
        attr_reader :exception
        attr_reader :responses

        # @api private
        # @since 2.1.0
        def initialize(exception, responses:)
          @exception = exception
          @responses = responses
        end

        # @api private
        # @since 2.1.0
        def rescue_response?
          responses.key?(exception.class.name)
        end

        # @api private
        # @since 2.1.0
        def status_code
          Rack::Utils.status_code(responses[exception.class.name])
        end
      end

      # @api private
      # @since 2.1.0
      def initialize(app, config, errors_app)
        @app = app
        @config = config
        @errors_app = errors_app
      end

      # @api private
      # @since 2.1.0
      def call(env)
        @app.call(env)
      rescue Exception => exception
        raise unless @config.render_errors

        render_exception(env, exception)
      end

      private

      def render_exception(env, exception)
        request = Rack::Request.new(env)
        renderable = RenderableException.new(exception, responses: @config.render_error_responses)

        status = renderable.status_code
        request.path_info = "/#{status}"
        request.set_header(Rack::REQUEST_METHOD, "GET")

        @errors_app.call(request.env)
      rescue Exception => exception
        # rubocop:disable Style/StderrPuts
        $stderr.puts "Error during exception rendering: #{exception}\n  #{exception.backtrace * "\n  "}"
        # rubocop:enable Style/StderrPuts

        [
          500,
          {"Content-Type" => "text/plain; charset=utf-8"},
          ["Internal Server Error"]
        ]
      end
    end
  end
end

# rubocop:enable Lint/RescueException


# frozen_string_literal: true

require "rack"

module Hanami
  module Middleware
    # The errors app given to {Hanami::Middleware::RenderErrors}, which renders error responses
    # from HTML pages kept in `public/` as simple JSON structures.
    #
    # @see Hanami::Middleware::RenderErrors
    #
    # @api private
    # @since 2.1.0
    class PublicErrorsApp
      # @api private
      # @since 2.1.0
      attr_reader :public_path

      # @api private
      # @since 2.1.0
      def initialize(public_path)
        @public_path = public_path
      end

      # @api private
      # @since 2.1.0
      def call(env)
        request = Rack::Request.new(env)
        status = request.path_info[1..].to_i
        content_type = request.get_header("HTTP_ACCEPT")

        default_body = {
          status: status,
          error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500])
        }

        render(status, content_type, default_body)
      end

      private

      def render(status, content_type, default_body)
        body, rendered_content_type = render_content(status, content_type, default_body)

        [
          status,
          {
            "Content-Type" => "#{rendered_content_type}; charset=utf-8",
            "Content-Length" => body.bytesize.to_s
          },
          [body]
        ]
      end

      def render_content(status, content_type, default_body)
        if content_type.to_s.start_with?("application/json")
          require "json"
          [JSON.generate(default_body), "application/json"]
        else
          [render_html_content(status, default_body), "text/html"]
        end
      end

      def render_html_content(status, default_body)
        path = "#{public_path}/#{status}.html"

        if File.exist?(path)
          File.read(path)
        else
          default_body[:error]
        end
      end
    end
  end
end


def render(status, content_type, default_body)
        body, rendered_content_type = render_content(status, content_type, default_body)

        [
          status,
          {
            "Content-Type" => "#{rendered_content_type}; charset=utf-8",
            "Content-Length" => body.bytesize.to_s
          },
          [body]
        ]
      end


# frozen_string_literal: true

module Hanami
  # @api private
  module Providers
    # Provider source to register Rack integration components in Hanami slices.
    #
    # @see Hanami::Providers::Logger
    # @see Hanami::Web::RackLogger
    # @see https://github.com/rack/rack
    # @see https://dry-rb.org/gems/dry-monitor/
    #
    # @api private
    # @since 2.0.0
    class Rack < Hanami::Provider::Source
      # @api private
      def prepare
        Dry::Monitor.load_extensions(:rack)

        # Explicitly register the Rack middleware events on our notifications bus. The Dry::Monitor
        # rack extension (activated above) does register these globally, but if the notifications
        # bus has been used before this provider loads, then it will have created its own separate
        # local copy of all registered events as of that moment in time, which will not be included
        # in the Rack events globally registered above.
        notifications = target["notifications"]
        notifications.register_event(Dry::Monitor::Rack::Middleware::REQUEST_START)
        notifications.register_event(Dry::Monitor::Rack::Middleware::REQUEST_STOP)
        notifications.register_event(Dry::Monitor::Rack::Middleware::REQUEST_ERROR)
      end

      # @api private
      def start
        slice.start :logger

        monitor_middleware = Dry::Monitor::Rack::Middleware.new(
          target["notifications"],
          clock: Dry::Monitor::Clock.new(unit: :microsecond)
        )

        rack_logger = Hanami::Web::RackLogger.new(target[:logger], env: slice.container.env)
        rack_logger.attach(monitor_middleware)

        register "monitor", monitor_middleware
      end
    end
  end
end


# frozen_string_literal: true

require "dry/configurable"
require "dry/core"
require "uri"
require_relative "../constants"

module Hanami
  module Providers
    # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity

    # @api private
    # @since 2.2.0
    class DB < Hanami::Provider::Source
      extend Dry::Core::Cache

      include Dry::Configurable(config_class: Providers::DB::Config)

      setting :adapters, mutable: true, default: Adapters.new
      setting :gateways, default: {}

      def initialize(...)
        super(...)

        @config_finalized = false
      end

      def finalize_config
        return if @config_finalized

        apply_parent_config if apply_parent_config?

        configure_gateways

        @config_finalized = true

        self
      end

      def prepare
        prepare_and_import_parent_db and return if import_from_parent?

        override_rom_inflector

        finalize_config

        require "hanami-db"

        gateways = prepare_gateways

        if gateways[:default]
          register "gateway", gateways[:default]
        elsif gateways.length == 1
          register "gateway", gateways.values.first
        end
        gateways.each do |key, gateway|
          register "gateways.#{key}", gateway
        end

        @rom_config = ROM::Configuration.new(gateways)

        config.each_plugin do |adapter_name, plugin_spec, config_block|
          if config_block
            @rom_config.plugin(adapter_name, plugin_spec) do |plugin_config|
              instance_exec(plugin_config, &config_block)
            end
          else
            @rom_config.plugin(adapter_name, plugin_spec)
          end
        end

        register "config", @rom_config
      end

      def start
        start_and_import_parent_db and return if import_from_parent?

        # Set up DB logging for the whole app. We share the app's notifications bus across all
        # slices, so we only need to configure the subsciprtion for DB logging just once.
        slice.app.start :db_logging

        # Register ROM components
        register_rom_components :relation, "relations"
        register_rom_components :command, File.join("db", "commands")
        register_rom_components :mapper, File.join("db", "mappers")

        rom = ROM.container(@rom_config)

        register "rom", rom
      end

      def stop
        slice["db.rom"].disconnect
      end

      # @api private
      # @since 2.2.0
      def database_urls
        finalize_config
        config.gateways.transform_values { _1.database_url }
      end

      private

      def parent_db_provider
        return @parent_db_provider if instance_variable_defined?(:@parent_db_provider)

        @parent_db_provider = slice.parent && slice.parent.container.providers[:db]
      end

      def apply_parent_config
        parent_db_provider.source.finalize_config

        self.class.settings.keys.each do |key|
          # Preserve settings already configured locally
          next if config.configured?(key)

          # Skip gateway config, we handle this in #configure_gateways
          next if key == :gateways

          # Skip adapter config, we handle this below
          next if key == :adapters

          config[key] = parent_db_provider.source.config[key]
        end

        parent_db_provider.source.config.adapters.each do |adapter_name, parent_adapter|
          adapter = config.adapter(adapter_name)

          adapter.class.settings.keys.each do |key|
            next if adapter.config.configured?(key)

            adapter.config[key] = parent_adapter.config[key].dup
          end
        end
      end

      def apply_parent_config?
        slice.config.db.configure_from_parent && parent_db_provider
      end

      def import_from_parent?
        slice.config.db.import_from_parent && slice.parent
      end

      def prepare_and_import_parent_db
        return unless parent_db_provider

        slice.parent.prepare :db
        @rom_config = slice.parent["db.config"]

        register "config", (@rom_config = slice.parent["db.config"])
        register "gateway", slice.parent["db.gateway"]
      end

      def start_and_import_parent_db
        return unless parent_db_provider

        slice.parent.start :db

        register "rom", slice.parent["db.rom"]
      end

      # ROM 5.3 doesn't have a configurable inflector.
      #
      # This is a problem in Hanami because using different
      # inflection rules for ROM will lead to constant loading
      # errors.
      def override_rom_inflector
        return if ROM::Inflector == Hanami.app["inflector"]

        ROM.instance_eval {
          remove_const :Inflector
          const_set :Inflector, Hanami.app["inflector"]
        }
      end

      def configure_gateways
        # Create gateway configs for gateways detected from database_url ENV vars
        database_urls_from_env = detect_database_urls_from_env
        database_urls_from_env.keys.each do |key|
          config.gateways[key] ||= Gateway.new
        end

        # Create a single default gateway if none is configured or detected from database URLs
        config.gateways[:default] = Gateway.new if config.gateways.empty?

        # Leave gateways in a stable order: :default first, followed by others in sort order
        if config.gateways.length > 1
          gateways = config.gateways
          config.gateways = {default: gateways[:default], **gateways.sort.to_h}.compact
        end

        config.gateways.each do |key, gw_config|
          gw_config.database_url ||= database_urls_from_env.fetch(key) {
            raise Hanami::ComponentLoadError, "A database_url for gateway #{key} is required to start :db."
          }

          ensure_database_gem(gw_config.database_url)

          apply_parent_gateway_config(key, gw_config) if apply_parent_config?

          gw_config.configure_adapter(config.adapters)
        end
      end

      def apply_parent_gateway_config(key, gw_config)
        parent_gw_config = parent_db_provider.source.config.gateways[key]

        # Only copy config from a parent gateway with the same name _and_ database URL
        return unless parent_gw_config&.database_url == gw_config.database_url

        # Copy config from matching parent gateway
        (gw_config.class.settings.keys - [:adapter]).each do |key|
          next if gw_config.config.configured?(key)

          gw_config.config[key] = parent_gw_config.config[key].dup
        end

        # If there is an adapter configured within this slice, prefer that, and do not copy the
        # adapter from the parent gateway
        unless config.adapters[gw_config.adapter_name] || gw_config.configured?(:adapter)
          gw_config.adapter = parent_gw_config.adapter.dup
        end
      end

      def prepare_gateways
        config.gateways.transform_values { |gw_config|
          # Avoid spurious connections by reusing identically configured gateways across slices
          fetch_or_store(gw_config.cache_keys) {
            ROM::Gateway.setup(
              gw_config.adapter_name,
              gw_config.database_url,
              **gw_config.options
            )
          }
        }
      end

      def detect_database_urls_from_env
        database_urls = {}

        env_var_prefix = slice.slice_name.name.gsub("/", "__").upcase + "__" unless slice.app?

        # Build gateway URLs from ENV vars with specific gateway named suffixes
        gateway_prefix = "#{env_var_prefix}DATABASE_URL__"
        ENV.select { |(k, _)| k.start_with?(gateway_prefix) }
          .each do |(var, _)|
            gateway_name = var.split(gateway_prefix).last.downcase

            database_urls[gateway_name.to_sym] = ENV[var]
          end

        # Set the default gateway from ENV var without suffix
        unless database_urls.key?(:default)
          fallback_vars = ["#{env_var_prefix}DATABASE_URL", "DATABASE_URL"].uniq

          fallback_vars.each do |var|
            url = ENV[var]
            database_urls[:default] = url and break if url
          end
        end

        if Hanami.env?(:test)
          database_urls.transform_values! { Hanami::DB::Testing.database_url(_1) }
        end

        database_urls
      end

      # @api private
      # @since 2.2.0
      DATABASE_GEMS = {
        "mysql2" => "mysql2",
        "postgres" => "pg",
        "sqlite" => "sqlite3"
      }.freeze
      private_constant :DATABASE_GEMS

      # Raises an error if the relevant database gem for the configured database_url is not
      # installed.
      #
      # Takes a conservative approach to raising errors. It only does so for the database_url
      # schemes generated by the `hanami new` CLI command. Uknown schemes are ignored and no errors
      # are raised.
      def ensure_database_gem(database_url)
        scheme = URI(database_url).scheme
        return unless scheme

        database_gem = DATABASE_GEMS[scheme]
        return unless database_gem

        return if Hanami.bundled?(database_gem)

        raise Hanami::ComponentLoadError, <<~STR
          The "#{database_gem}" gem is required to connect to #{database_url}. Please add it to your Gemfile.
        STR
      end

      def register_rom_components(component_type, path)
        components_path = target.source_path.join(path)
        components_path.glob("**/*.rb").each do |component_file|
          component_name = component_file
            .relative_path_from(components_path)
            .sub(RB_EXT_REGEXP, "")
            .to_s

          component_class = target.inflector.camelize(
            "#{target.slice_name.name}/#{path}/#{component_name}"
          ).then { target.inflector.constantize(_1) }

          @rom_config.public_send(:"register_#{component_type}", component_class)
        end
      end
    end
    # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity

    Dry::System.register_provider_source(
      :db,
      source: DB,
      group: :hanami,
      provider_options: {namespace: true}
    )
  end
end


def apply_parent_config
        parent_db_provider.source.finalize_config

        self.class.settings.keys.each do |key|
          # Preserve settings already configured locally
          next if config.configured?(key)

          # Skip gateway config, we handle this in #configure_gateways
          next if key == :gateways

          # Skip adapter config, we handle this below
          next if key == :adapters

          config[key] = parent_db_provider.source.config[key]
        end

        parent_db_provider.source.config.adapters.each do |adapter_name, parent_adapter|
          adapter = config.adapter(adapter_name)

          adapter.class.settings.keys.each do |key|
            next if adapter.config.configured?(key)

            adapter.config[key] = parent_adapter.config[key].dup
          end
        end
      end


def configure_gateways
        # Create gateway configs for gateways detected from database_url ENV vars
        database_urls_from_env = detect_database_urls_from_env
        database_urls_from_env.keys.each do |key|
          config.gateways[key] ||= Gateway.new
        end

        # Create a single default gateway if none is configured or detected from database URLs
        config.gateways[:default] = Gateway.new if config.gateways.empty?

        # Leave gateways in a stable order: :default first, followed by others in sort order
        if config.gateways.length > 1
          gateways = config.gateways
          config.gateways = {default: gateways[:default], **gateways.sort.to_h}.compact
        end

        config.gateways.each do |key, gw_config|
          gw_config.database_url ||= database_urls_from_env.fetch(key) {
            raise Hanami::ComponentLoadError, "A database_url for gateway #{key} is required to start :db."
          }

          ensure_database_gem(gw_config.database_url)

          apply_parent_gateway_config(key, gw_config) if apply_parent_config?

          gw_config.configure_adapter(config.adapters)
        end
      end


def apply_parent_gateway_config(key, gw_config)
        parent_gw_config = parent_db_provider.source.config.gateways[key]

        # Only copy config from a parent gateway with the same name _and_ database URL
        return unless parent_gw_config&.database_url == gw_config.database_url

        # Copy config from matching parent gateway
        (gw_config.class.settings.keys - [:adapter]).each do |key|
          next if gw_config.config.configured?(key)

          gw_config.config[key] = parent_gw_config.config[key].dup
        end

        # If there is an adapter configured within this slice, prefer that, and do not copy the
        # adapter from the parent gateway
        unless config.adapters[gw_config.adapter_name] || gw_config.configured?(:adapter)
          gw_config.adapter = parent_gw_config.adapter.dup
        end
      end


def prepare_gateways
        config.gateways.transform_values { |gw_config|
          # Avoid spurious connections by reusing identically configured gateways across slices
          fetch_or_store(gw_config.cache_keys) {
            ROM::Gateway.setup(
              gw_config.adapter_name,
              gw_config.database_url,
              **gw_config.options
            )
          }
        }
      end


def detect_database_urls_from_env
        database_urls = {}

        env_var_prefix = slice.slice_name.name.gsub("/", "__").upcase + "__" unless slice.app?

        # Build gateway URLs from ENV vars with specific gateway named suffixes
        gateway_prefix = "#{env_var_prefix}DATABASE_URL__"
        ENV.select { |(k, _)| k.start_with?(gateway_prefix) }
          .each do |(var, _)|
            gateway_name = var.split(gateway_prefix).last.downcase

            database_urls[gateway_name.to_sym] = ENV[var]
          end

        # Set the default gateway from ENV var without suffix
        unless database_urls.key?(:default)
          fallback_vars = ["#{env_var_prefix}DATABASE_URL", "DATABASE_URL"].uniq

          fallback_vars.each do |var|
            url = ENV[var]
            database_urls[:default] = url and break if url
          end
        end

        if Hanami.env?(:test)
          database_urls.transform_values! { Hanami::DB::Testing.database_url(_1) }
        end

        database_urls
      end


def ensure_database_gem(database_url)
        scheme = URI(database_url).scheme
        return unless scheme

        database_gem = DATABASE_GEMS[scheme]
        return unless database_gem

        return if Hanami.bundled?(database_gem)

        raise Hanami::ComponentLoadError, <<~STR
          The "#{database_gem}" gem is required to connect to #{database_url}. Please add it to your Gemfile.
        STR
      end


def register_rom_components(component_type, path)
        components_path = target.source_path.join(path)
        components_path.glob("**/*.rb").each do |component_file|
          component_name = component_file
            .relative_path_from(components_path)
            .sub(RB_EXT_REGEXP, "")
            .to_s

          component_class = target.inflector.camelize(
            "#{target.slice_name.name}/#{path}/#{component_name}"
          ).then { target.inflector.constantize(_1) }

          @rom_config.public_send(:"register_#{component_type}", component_class)
        end
      end


# frozen_string_literal: true

module Hanami
  module Providers
    # @api private
    class I18n < Hanami::Provider::Source
      SLICE_CONFIGURED_SETTINGS = %i[default_locale available_locales load_path fallbacks].freeze

      DEFAULT_LOCALE = :en
      DEFAULT_AVAILABLE_LOCALES = [].freeze
      DEFAULT_LOAD_PATH = ["config/i18n/**/*.{yml,yaml,json,rb}"].freeze

      setting :default_locale, default: DEFAULT_LOCALE
      setting :available_locales, default: DEFAULT_AVAILABLE_LOCALES
      setting :backend
      setting :load_path, default: DEFAULT_LOAD_PATH
      setting :fallbacks

      def prepare
        require "i18n"

        SLICE_CONFIGURED_SETTINGS.each do |setting|
          next if config.configured?(setting)

          config.public_send(:"#{setting}=", slice.config.i18n.public_send(setting))
        end
      end

      # rubocop:disable Metrics/AbcSize
      def start
        backend = config.backend || ::I18n::Backend::Simple.new

        # Configure fallbacks if enabled (only for default backend, not custom backends)
        fallbacks_config = nil
        if config.fallbacks && !config.backend
          fallbacks_config =
            case config.fallbacks
            when true
              # Use default_locale as the only fallback
              fallbacks = ::I18n::Locale::Fallbacks.new
              fallbacks.defaults = [config.default_locale]
              fallbacks
            when Hash
              # Use explicit per-locale fallback mapping
              ::I18n::Locale::Fallbacks.new(config.fallbacks)
            when Array
              # Use the given fallbacks for all locales
              fallbacks = ::I18n::Locale::Fallbacks.new
              fallbacks.defaults = config.fallbacks
              fallbacks
            else
              ::I18n::Locale::Fallbacks.new(config.fallbacks)
            end
        end

        # Only load translation files if using the default backend. Custom backends are expected to
        # handle their own initialization.
        unless config.backend
          translation_files = resolve_load_paths(Array(config.load_path))
          backend.load_translations(*translation_files) if translation_files.any?
        end

        register "i18n", Backend.new(
          backend,
          locale: config.default_locale,
          default_locale: config.default_locale,
          available_locales: config.available_locales,
          fallbacks: fallbacks_config
        )
      end
      # rubocop:enable Metrics/AbcSize

      private

      # Resolves load path patterns to actual file paths. Relative patterns are resolved against
      # slice.root. Absolute paths are used as-is.
      def resolve_load_paths(patterns)
        patterns.flat_map do |pattern|
          if absolute_path?(pattern)
            # Absolute path or already-globbed absolute paths
            File.exist?(pattern) ? [pattern] : Dir.glob(pattern)
          else
            # Relative pattern - resolve against slice/app root
            Dir.glob(slice.root.join(pattern))
          end
        end
      end

      def absolute_path?(path)
        Pathname.new(path).absolute?
      end
    end

    Dry::System.register_provider_source(
      :i18n,
      source: I18n,
      group: :hanami
    )
  end
end

require_relative "i18n/backend"


# frozen_string_literal: true

require "hanami/router"
require_relative "routing/resolver"
require_relative "routing/middleware/stack"

module Hanami
  class Slice
    # `Hanami::Router` subclass with enhancements for use within Hanami apps.
    #
    # This is loaded from Hanami apps and slices and made available as their
    # {Hanami::Slice::ClassMethods#router router}.
    #
    # @api private
    class Router < ::Hanami::Router
      # @api private
      attr_reader :inflector

      # @api private
      attr_reader :middleware_stack

      # @api private
      attr_reader :path_prefix

      # @api private
      def initialize(
        routes:,
        inflector:,
        middleware_stack: Routing::Middleware::Stack.new,
        prefix: ::Hanami::Router::DEFAULT_PREFIX,
        **kwargs,
        &blk
      )
        @path_prefix = Hanami::Router::Prefix.new(prefix)
        @inflector = inflector
        @middleware_stack = middleware_stack
        @resource_scope = []
        instance_eval(&blk)
        super(**kwargs, &routes)
      end

      # @api private
      def freeze
        return self if frozen?

        remove_instance_variable(:@middleware_stack)
        super
      end

      # @api private
      def to_rack_app
        middleware_stack.to_rack_app(self)
      end

      # @api private
      def use(*args, **kwargs, &blk)
        middleware_stack.use(*args, **kwargs.merge(path_prefix: path_prefix.to_s), &blk)
      end

      # Yields a block for routes to resolve their action components from the given slice.
      #
      # An optional URL prefix may be supplied with `at:`.
      #
      # @example
      #   # config/routes.rb
      #
      #   module MyApp
      #     class Routes < Hanami::Routes
      #       slice :admin, at: "/admin" do
      #         # Will route to the "actions.posts.index" component in Admin::Slice
      #         get "posts", to: "posts.index"
      #       end
      #     end
      #   end
      #
      # @param slice_name [Symbol] the slice's name
      # @param at [String, nil] optional URL prefix for the routes
      #
      # @api public
      # @since 2.0.0
      def slice(slice_name, at:, as: nil, &blk)
        blk ||= @resolver.find_slice(slice_name).routes

        prev_resolver = @resolver
        @resolver = @resolver.to_slice(slice_name)

        scope(at, as:, &blk)
      ensure
        @resolver = prev_resolver
      end

      # Generates RESTful routes for a plural resource.
      #
      # @param name [Symbol] the resource name (plural)
      # @param options [Hash] options for customizing the routes
      # @option options [Array<Symbol>] :only Limit to specific actions
      # @option options [Array<Symbol>] :except Exclude specific actions
      # @option options [String] :to the action key namespace, e.g. "namespace.action"
      # @option options [String] :path the URL path
      # @option options [String, Symbol] :as the route name prefix
      #
      # @example
      #   resources :users
      #   # Generates:
      #   # GET    /users          users.index
      #   # GET    /users/new      users.new
      #   # POST   /users          users.create
      #   # GET    /users/:id      users.show
      #   # GET    /users/:id/edit users.edit
      #   # PATCH  /users/:id      users.update
      #   # DELETE /users/:id      users.destroy
      #
      # @api public
      # @since 2.3.0
      def resources(name, **options, &block)
        build_resource(name, :plural, options, &block)
      end

      # Generates RESTful routes for a singular resource.
      #
      # @param name [Symbol] the resource name (singular)
      # @param options [Hash] options for customizing the routes
      # @option options [Array<Symbol>] :only limit to specific actions
      # @option options [Array<Symbol>] :except exclude specific actions
      # @option options [String] :to the action key namespace, e.g. "namespace.action"
      # @option options [String] :path the URL path
      # @option options [String, Symbol] :as the route name prefix
      #
      # @example
      #   resource :profile
      #   # Generates (singular, no index):
      #   # GET    /profile/new    profile.new
      #   # POST   /profile        profile.create
      #   # GET    /profile        profile.show
      #   # GET    /profile/edit   profile.edit
      #   # PATCH  /profile        profile.update
      #   # DELETE /profile        profile.destroy
      #
      # @api public
      # @since 2.3.0
      def resource(name, **options, &block)
        build_resource(name, :singular, options, &block)
      end

      private

      def build_resource(name, type, options, &block)
        resource_builder = ResourceBuilder.new(
          name: name,
          type: type,
          resource_scope: @resource_scope,
          options: options,
          inflector: inflector
        )

        resource_builder.add_routes(self)
        resource_builder.scope(self, &block) if block
      end

      # Builds RESTful routes for a resource
      #
      # @api private
      class ResourceBuilder
        ROUTE_OPTIONS = {
          index: {method: :get},
          new: {method: :get, path_suffix: "/new", name_prefix: "new"},
          create: {method: :post},
          show: {method: :get, path_suffix: "/:id"},
          edit: {method: :get, path_suffix: "/:id/edit", name_prefix: "edit"},
          update: {method: :patch, path_suffix: "/:id"},
          destroy: {method: :delete, path_suffix: "/:id"}
        }.freeze

        PLURAL_ACTIONS = %i[index new create show edit update destroy].freeze
        SINGULAR_ACTIONS = (PLURAL_ACTIONS - %i[index]).freeze

        def initialize(name:, type:, resource_scope:, options:, inflector:)
          @name = name
          @type = type
          @resource_scope = resource_scope
          @options = options
          @inflector = inflector

          @path = options[:path] || name.to_s
        end

        def scope(router, &block)
          @resource_scope.push(@name)
          router.scope(scope_path, as: scope_name, &block)
        ensure
          @resource_scope.pop
        end

        def add_routes(router)
          actions.each do |action|
            add_route(router, action, ROUTE_OPTIONS.fetch(action))
          end
        end

        private

        def plural?
          @type == :plural
        end

        def singular?
          !plural?
        end

        def scope_path
          if plural?
            "#{@path}/:#{@inflector.singularize(@path.to_s)}_id"
          else
            @path
          end
        end

        def scope_name
          @inflector.singularize(@name)
        end

        def actions
          default_actions = plural? ? PLURAL_ACTIONS : SINGULAR_ACTIONS
          if @options[:only]
            Array(@options[:only]) & default_actions
          elsif @options[:except]
            default_actions - Array(@options[:except])
          else
            default_actions
          end
        end

        def add_route(router, action, route_options)
          path = "/#{@path}#{route_suffix(route_options[:path_suffix])}"
          to = "#{key_path_base}#{CONTAINER_KEY_DELIMITER}#{action}"
          as = route_name(action, route_options[:name_prefix])

          router.public_send(route_options[:method], path, to:, as:)
        end

        def route_suffix(suffix)
          return suffix.sub(LEADING_ID_REGEX, "") if suffix && singular?

          suffix
        end
        LEADING_ID_REGEX = %r{\A/:id}

        def key_path_base
          @key_path_base ||= @options[:to] || begin
            if @resource_scope.any?
              prefix = @resource_scope.join(CONTAINER_KEY_DELIMITER)
              "#{prefix}#{CONTAINER_KEY_DELIMITER}#{@name}"
            else
              @name.to_s
            end
          end
        end

        def route_name(action, prefix)
          name = route_name_base
          name = @inflector.pluralize(name) if plural? && PLURALIZED_NAME_ACTIONS.include?(action)

          [prefix, name]
        end
        PLURALIZED_NAME_ACTIONS = %i[index create].freeze

        def route_name_base
          @route_name_base ||=
            if @options[:as]
              @options[:as].to_s
            elsif plural?
              @inflector.singularize(@name.to_s)
            else
              @name.to_s
            end
        end
      end
    end
  end
end


def build_resource(name, type, options, &block)
        resource_builder = ResourceBuilder.new(
          name: name,
          type: type,
          resource_scope: @resource_scope,
          options: options,
          inflector: inflector
        )

        resource_builder.add_routes(self)
        resource_builder.scope(self, &block) if block
      end


def actions
          default_actions = plural? ? PLURAL_ACTIONS : SINGULAR_ACTIONS
          if @options[:only]
            Array(@options[:only]) & default_actions
          elsif @options[:except]
            default_actions - Array(@options[:except])
          else
            default_actions
          end
        end


def key_path_base
          @key_path_base ||= @options[:to] || begin
            if @resource_scope.any?
              prefix = @resource_scope.join(CONTAINER_KEY_DELIMITER)
              "#{prefix}#{CONTAINER_KEY_DELIMITER}#{@name}"
            else
              @name.to_s
            end
          end
        end


def route_name_base
          @route_name_base ||=
            if @options[:as]
              @options[:as].to_s
            elsif plural?
              @inflector.singularize(@name.to_s)
            else
              @name.to_s
            end
        end