# 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

render

      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


# 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

gateways

config.gateways.length > 1
!(config.gateways.length > 1)


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"

start

fallbacks

config.fallbacks && !config.backend
!(config.fallbacks && !config.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


# frozen_string_literal: true

require "hanami/view"

module Hanami
  module Helpers
    # Helper methods for generating HTML forms.
    #
    # These helpers will be automatically available in your view templates, part classes and scope
    # classes.
    #
    # This module provides one primary method: {#form_for}, yielding an HTML form builder. This
    # integrates with request params and template locals to populate the form with appropriate
    # values.
    #
    # @api public
    # @since 2.1.0
    module FormHelper
      require_relative "form_helper/form_builder"

      # Default HTTP method for form
      #
      # @since 2.1.0
      # @api private
      DEFAULT_METHOD = "POST"

      # Default charset
      #
      # @since 2.1.0
      # @api private
      DEFAULT_CHARSET = "utf-8"

      # CSRF Token session key
      #
      # This name of this key is shared with the hanami and hanami-action gems.
      #
      # @since 2.1.0
      # @api private
      CSRF_TOKEN = :_csrf_token

      include Hanami::View::Helpers::TagHelper

      # Yields a form builder for constructing an HTML form and returns the resulting form string.
      #
      # See {FormHelper::FormBuilder} for the methods for building the form's fields.
      #
      # @overload form_for(base_name, url, values: _form_for_values, params: _form_for_params, **attributes)
      #   Builds the form using the given base name for all fields.
      #
      #   @param base_name [String] the base
      #   @param url [String] the URL for submitting the form
      #   @param values [Hash] values to be used for populating form field values; optional,
      #     defaults to the template's locals or to a part's `{name => self}`
      #   @param params [Hash] request param values to be used for populating form field values;
      #     these are used in preference over the `values`; optional, defaults to the current
      #     request's params
      #   @param attributes [Hash] the HTML attributes for the form tag
      #   @yieldparam [FormHelper::FormBuilder] f the form builder
      #
      # @overload form_for(url, values: _form_for_values, params: _form_for_params, **attributes)
      #   @param url [String] the URL for submitting the form
      #   @param values [Hash] values to be used for populating form field values; optional,
      #     defaults to the template's locals or to a part's `{name => self}`
      #   @param params [Hash] request param values to be used for populating form field values;
      #     these are used in preference over the `values`; optional, defaults to the current
      #     request's params
      #   @param attributes [Hash] the HTML attributes for the form tag
      #   @yieldparam [FormHelper::FormBuilder] f the form builder
      #
      # @return [String] the form HTML
      #
      # @see FormHelper
      # @see FormHelper::FormBuilder
      #
      # @example Basic usage
      #   <%= form_for("book", "/books", class: "form-horizontal") do |f| %>
      #

      #       <%= f.label "title" %>
      #       <%= f.text_field "title", class: "form-control" %>
      #

      #
      #     <%= f.submit "Create" %>
      #   <% end %>
      #
      #   =>
      #   <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
      #     <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
      #

      #       Title
      #       <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
      #

      #
      #     <button type="submit">Create</button>
      #   </form>
      #
      # @example Without base name
      #
      #   <%= form_for("/books", class: "form-horizontal") do |f| %>
      #

      #       <%= f.label "books.title" %>
      #       <%= f.text_field "books.title", class: "form-control" %>
      #

      #
      #     <%= f.submit "Create" %>
      #   <% end %>
      #
      #   =>
      #   <form action="/books" method="POST" accept-charset="utf-8" class="form-horizontal">
      #     <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
      #

      #       Title
      #       <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
      #

      #
      #     <button type="submit">Create</button>
      #   </form>
      #
      # @example Method override
      #   <%= form_for("/books/123", method: :put) do |f|
      #     <%= f.text_field "book.title" %>
      #     <%= f.submit "Update" %>
      #   <% end %>
      #
      #   =>
      #   <form action="/books/123" accept-charset="utf-8" method="POST">
      #     <input type="hidden" name="_method" value="PUT">
      #     <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
      #     <input type="text" name="book[title]" id="book-title" value="Test Driven Development">
      #
      #     <button type="submit">Update</button>
      #   </form>
      #
      # @example Overriding values
      #   <%= form_for("/songs", values: {song: {title: "Envision"}}) do |f| %>
      #     <%= f.text_field "song.title" %>
      #     <%= f.submit "Create" %>
      #   <%= end %>
      #
      #   =>
      #   <form action="/songs" accept-charset="utf-8" method="POST">
      #     <input type="hidden" name="_csrf_token" value="920cd5bfaecc6e58368950e790f2f7b4e5561eeeab230aa1b7de1b1f40ea7d5d">
      #     <input type="text" name="song[title]" id="song-title" value="Envision">
      #
      #     <button type="submit">Create</button>
      #   </form>
      #
      # @api public
      # @since 2.1.0
      def form_for(base_name, url = nil, values: _form_for_values, params: _form_for_params, **attributes)
        url, base_name = base_name, nil if url.nil?

        values = Values.new(values: values, params: params, csrf_token: _form_csrf_token)

        builder = FormBuilder.new(
          base_name: base_name,
          values: values,
          inflector: _context.inflector,
          form_attributes: attributes
        )

        content = (block_given? ? yield(builder) : "").html_safe

        builder.call(content, action: url, **attributes)
      end

      # Returns CSRF meta tags for use via unobtrusive JavaScript (UJS) libraries.
      #
      # @return [String, nil] the tags, if a CSRF token is available, or nil
      #
      # @example
      #   csrf_meta_tags
      #
      #   =>
      #   <meta name="csrf-param" content="_csrf_token">
      #   <meta name="csrf-token" content="4a038be85b7603c406dcbfad4b9cdf91ec6ca138ed6441163a07bb0fdfbe25b5">
      #
      # @api public
      # @since 2.1.0
      def csrf_meta_tags
        return unless (token = _form_csrf_token)

        tag.meta(name: "csrf-param", content: CSRF_TOKEN) +
          tag.meta(name: "csrf-token", content: token)
      end

      # @api private
      # @since 2.1.0
      def _form_for_values
        if respond_to?(:_locals) # Scope
          _locals
        elsif respond_to?(:_name) # Part
          {_name => self}
        else
          {}
        end
      end

      # @api private
      # @since 2.1.0
      def _form_for_params
        _context.request.params
      end

      # @since 2.1.0
      # @api private
      def _form_csrf_token
        return unless _context.request.session_enabled?

        _context.csrf_token
      end
    end
  end
end

call

        def call(content, **attributes)
          attributes["accept-charset"] ||= DEFAULT_CHARSET

          method_override, original_form_method = _form_method(attributes)
          csrf_token, token = _csrf_token(values, attributes)

          tag.form(**attributes) do
            (+"").tap { |inner|
              inner << input(type: "hidden", name: "_method", value: original_form_method) if method_override
              inner << input(type: "hidden", name: "_csrf_token", value: token) if csrf_token
              inner << content
            }.html_safe
          end
        end


# frozen_string_literal: true

require "uri"
require "hanami/view"
require_relative "../constants"

module Hanami
  module Helpers
    # HTML assets helpers
    #
    # Inject these helpers in a view
    #
    # @since 2.1.0
    #
    # @see http://www.rubydoc.info/gems/hanami-helpers/Hanami/Helpers/HtmlHelper
    module AssetsHelper
      # @since 2.1.0
      # @api private
      NEW_LINE_SEPARATOR = "\n"

      # @since 2.1.0
      # @api private
      WILDCARD_EXT = ".*"

      # @since 2.1.0
      # @api private
      JAVASCRIPT_EXT = ".js"

      # @since 2.1.0
      # @api private
      STYLESHEET_EXT = ".css"

      # @since 2.1.0
      # @api private
      JAVASCRIPT_MIME_TYPE = "text/javascript"

      # @since 2.1.0
      # @api private
      STYLESHEET_MIME_TYPE = "text/css"

      # @since 2.1.0
      # @api private
      FAVICON_MIME_TYPE = "image/x-icon"

      # @since 2.1.0
      # @api private
      STYLESHEET_REL = "stylesheet"

      # @since 2.1.0
      # @api private
      FAVICON_REL = "shortcut icon"

      # @since 2.1.0
      # @api private
      DEFAULT_FAVICON = "favicon.ico"

      # @since 0.3.0
      # @api private
      CROSSORIGIN_ANONYMOUS = "anonymous"

      # @since 0.3.0
      # @api private
      # TODO: we can drop the defined?-check and fallback once Ruby 3.3 becomes our minimum required version
      ABSOLUTE_URL_MATCHER = (
        defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
      ).make_regexp

      # @since 1.1.0
      # @api private
      QUERY_STRING_MATCHER = /\?/

      include Hanami::View::Helpers::TagHelper

      # Generate `script` tag for given source(s)
      #
      # It accepts one or more strings representing the name of the asset, if it
      # comes from the application or third party gems. It also accepts strings
      # representing absolute URLs in case of public CDN (eg. jQuery CDN).
      #
      # If the "fingerprint mode" is on, `src` is the fingerprinted
      # version of the relative URL.
      #
      # If the "CDN mode" is on, the `src` is an absolute URL of the
      # application CDN.
      #
      # If the "subresource integrity mode" is on, `integrity` is the
      # name of the algorithm, then a hyphen, then the hash value of the file.
      # If more than one algorithm is used, they"ll be separated by a space.
      #
      # If the Content Security Policy uses 'nonce' and the source is not
      # absolute, the nonce value of the current request is automatically added
      # as an attribute. You can override this with the `nonce: false` option.
      # See {#content_security_policy_nonce} for more.
      #
      # @param sources [Array<String, #url>] one or more assets by name or absolute URL
      #
      # @return [Hanami::View::HTML::SafeString] the markup
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the javascript file is missing
      # from the manifest
      #
      # @since 2.1.0
      #
      # @see Hanami::Assets::Helpers#path
      #
      # @example Single Asset
      #
      #   <%= javascript_tag "application" %>
      #
      #   #
      #
      # @example Multiple Assets
      #
      #   <%= javascript_tag "application", "dashboard" %>
      #
      #   #
      #   #
      #
      # @example Asynchronous Execution
      #
      #   <%= javascript_tag "application", async: true %>
      #
      #   #
      #
      # @example Subresource Integrity
      #
      #   <%= javascript_tag "application" %>
      #
      #   #
      #
      # @example Subresource Integrity for 3rd Party Scripts
      #
      #   <%= javascript_tag "https://example.com/assets/example.js", integrity: "sha384-oqVu...Y8wC" %>
      #
      #   #
      #
      # @example Deferred Execution
      #
      #   <%= javascript_tag "application", defer: true %>
      #
      #   #
      #
      # @example Disable nonce
      #
      #   <%= javascript_tag "application", nonce: false %>
      #
      # @example Absolute URL
      #
      #   <%= javascript_tag "https://code.jquery.com/jquery-2.1.4.min.js" %>
      #
      #   #
      #
      # @example Fingerprint Mode
      #
      #   <%= javascript_tag "application" %>
      #
      #   #
      #
      # @example CDN Mode
      #
      #   <%= javascript_tag "application" %>
      #
      #   #
      def javascript_tag(*sources, **options)
        options = options.reject { |k, _| k.to_sym == :src }
        nonce_option = options.delete(:nonce)

        _safe_tags(*sources) do |source|
          attributes = {
            src: _typed_url(source, JAVASCRIPT_EXT),
            type: JAVASCRIPT_MIME_TYPE,
            nonce: _nonce(source, nonce_option)
          }
          attributes.merge!(options)

          if _context.assets.subresource_integrity? || attributes.include?(:integrity)
            attributes[:integrity] ||= _subresource_integrity_value(source, JAVASCRIPT_EXT)
            attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
          end

          tag.script(**attributes).to_s
        end
      end

      # Generate `link` tag for given source(s)
      #
      # It accepts one or more strings representing the name of the asset, if it
      # comes from the application or third party gems. It also accepts strings
      # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
      #
      # If the "fingerprint mode" is on, `href` is the fingerprinted
      # version of the relative URL.
      #
      # If the "CDN mode" is on, the `href` is an absolute URL of the
      # application CDN.
      #
      # If the "subresource integrity mode" is on, `integrity` is the
      # name of the algorithm, then a hyphen, then the hashed value of the file.
      # If more than one algorithm is used, they"ll be separated by a space.
      #
      # If the Content Security Policy uses 'nonce' and the source is not
      # absolute, the nonce value of the current request is automatically added
      # as an attribute. You can override this with the `nonce: false` option.
      # See {#content_security_policy_nonce} for more.
      #
      # @param sources [Array<String, #url>] one or more assets by name or absolute URL
      #
      # @return [Hanami::View::HTML::SafeString] the markup
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the stylesheet file is missing
      # from the manifest
      #
      # @since 2.1.0
      #
      # @see Hanami::Assets::Helpers#path
      #
      # @example Single Asset
      #
      #   <%= stylesheet_tag "application" %>
      #
      #   # <link href="/assets/application.css" type="text/css" rel="stylesheet">
      #
      # @example Multiple Assets
      #
      #   <%= stylesheet_tag "application", "dashboard" %>
      #
      #   # <link href="/assets/application.css" type="text/css" rel="stylesheet">
      #   # <link href="/assets/dashboard.css" type="text/css" rel="stylesheet">
      #
      # @example Disable nonce
      #
      #   <%= stylesheet_tag "application", nonce: false %>
      #
      # @example Subresource Integrity
      #
      #   <%= stylesheet_tag "application" %>
      #
      #   # <link href="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
      #   #       type="text/css" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
      #
      # @example Subresource Integrity for 3rd Party Assets
      #
      #   <%= stylesheet_tag "https://example.com/assets/example.css", integrity: "sha384-oqVu...Y8wC" %>
      #
      #   # <link href="https://example.com/assets/example.css"
      #   #       type="text/css" rel="stylesheet" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
      #
      # @example Absolute URL
      #
      #   <%= stylesheet_tag "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" %>
      #
      #   # <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
      #   #       type="text/css" rel="stylesheet">
      #
      # @example Fingerprint Mode
      #
      #   <%= stylesheet_tag "application" %>
      #
      #   # <link href="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css" type="text/css" rel="stylesheet">
      #
      # @example CDN Mode
      #
      #   <%= stylesheet_tag "application" %>
      #
      #   # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
      #   #       type="text/css" rel="stylesheet">
      def stylesheet_tag(*sources, **options)
        options = options.reject { |k, _| k.to_sym == :href }
        nonce_option = options.delete(:nonce)

        _safe_tags(*sources) do |source|
          attributes = {
            href: _typed_url(source, STYLESHEET_EXT),
            type: STYLESHEET_MIME_TYPE,
            rel: STYLESHEET_REL,
            nonce: _nonce(source, nonce_option)
          }
          attributes.merge!(options)

          if _context.assets.subresource_integrity? || attributes.include?(:integrity)
            attributes[:integrity] ||= _subresource_integrity_value(source, STYLESHEET_EXT)
            attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
          end

          tag.link(**attributes).to_s
        end
      end

      # Generate `img` tag for given source
      #
      # It accepts one string representing the name of the asset, if it comes
      # from the application or third party gems. It also accepts strings
      # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
      #
      # `alt` Attribute is auto generated from `src`.
      # You can specify a different value, by passing the `:src` option.
      #
      # If the "fingerprint mode" is on, `src` is the fingerprinted
      # version of the relative URL.
      #
      # If the "CDN mode" is on, the `src` is an absolute URL of the
      # application CDN.
      #
      # @param source [String, #url] asset name, absolute URL, or asset object
      # @param options [Hash] HTML 5 attributes
      #
      # @return [Hanami::View::HTML::SafeString] the markup
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the image file is missing
      # from the manifest
      #
      # @since 2.1.0
      #
      # @see Hanami::Assets::Helpers#path
      #
      # @example Basic Usage
      #
      #   <%= image_tag "logo.png" %>
      #
      #   #
      #
      # @example Custom alt Attribute
      #
      #   <%= image_tag "logo.png", alt: "Application Logo" %>
      #
      #   #
      #
      # @example Custom HTML Attributes
      #
      #   <%= image_tag "logo.png", id: "logo", class: "image" %>
      #
      #   #
      #
      # @example Absolute URL
      #
      #   <%= image_tag "https://example-cdn.com/images/logo.png" %>
      #
      #   #
      #
      # @example Fingerprint Mode
      #
      #   <%= image_tag "logo.png" %>
      #
      #   #
      #
      # @example CDN Mode
      #
      #   <%= image_tag "logo.png" %>
      #
      #   #
      def image_tag(source, options = {})
        options = options.reject { |k, _| k.to_sym == :src }
        attributes = {
          src: asset_url(source),
          alt: _context.inflector.humanize(::File.basename(source, WILDCARD_EXT))
        }
        attributes.merge!(options)

        tag.img(**attributes)
      end

      # Generate `link` tag application favicon.
      #
      # If no argument is given, it assumes `favico.ico` from the application.
      #
      # It accepts one string representing the name of the asset.
      #
      # If the "fingerprint mode" is on, `href` is the fingerprinted version
      # of the relative URL.
      #
      # If the "CDN mode" is on, the `href` is an absolute URL of the
      # application CDN.
      #
      # @param source [String, #url] asset name or asset object
      # @param options [Hash] HTML 5 attributes
      #
      # @return [Hanami::View::HTML::SafeString] the markup
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the favicon is file missing
      # from the manifest
      #
      # @since 2.1.0
      #
      # @see Hanami::Assets::Helpers#path
      #
      # @example Basic Usage
      #
      #   <%= favicon_tag %>
      #
      #   # <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
      #
      # @example Custom Path
      #
      #   <%= favicon_tag "fav.ico" %>
      #
      #   # <link href="/assets/fav.ico" rel="shortcut icon" type="image/x-icon">
      #
      # @example Custom HTML Attributes
      #
      #   <%= favicon_tag "favicon.ico", id: "fav" %>
      #
      #   # <link id="fav" href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
      #
      # @example Fingerprint Mode
      #
      #   <%= favicon_tag %>
      #
      #   # <link href="/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico" rel="shortcut icon" type="image/x-icon">
      #
      # @example CDN Mode
      #
      #   <%= favicon_tag %>
      #
      #   # <link href="https://assets.bookshelf.org/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico"
      #           rel="shortcut icon" type="image/x-icon">
      def favicon_tag(source = DEFAULT_FAVICON, options = {})
        options = options.reject { |k, _| k.to_sym == :href }

        attributes = {
          href: asset_url(source),
          rel: FAVICON_REL,
          type: FAVICON_MIME_TYPE
        }
        attributes.merge!(options)

        tag.link(**attributes)
      end

      # Generate `video` tag for given source
      #
      # It accepts one string representing the name of the asset, if it comes
      # from the application or third party gems. It also accepts strings
      # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
      #
      # Alternatively, it accepts a block that allows to specify one or more
      # sources via the `source` tag.
      #
      # If the "fingerprint mode" is on, `src` is the fingerprinted
      # version of the relative URL.
      #
      # If the "CDN mode" is on, the `src` is an absolute URL of the
      # application CDN.
      #
      # @param source [String, #url] asset name, absolute URL or asset object
      # @param options [Hash] HTML 5 attributes
      #
      # @return [Hanami::View::HTML::SafeString] the markup
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the video file is missing
      # from the manifest
      #
      # @raise [ArgumentError] if source isn"t specified both as argument or
      #   tag inside the given block
      #
      # @since 2.1.0
      #
      # @see Hanami::Assets::Helpers#path
      #
      # @example Basic Usage
      #
      #   <%= video_tag "movie.mp4" %>
      #
      #   # <video src="/assets/movie.mp4"></video>
      #
      # @example Absolute URL
      #
      #   <%= video_tag "https://example-cdn.com/assets/movie.mp4" %>
      #
      #   # <video src="https://example-cdn.com/assets/movie.mp4"></video>
      #
      # @example Custom HTML Attributes
      #
      #   <%= video_tag("movie.mp4", autoplay: true, controls: true) %>
      #
      #   # <video src="/assets/movie.mp4" autoplay="autoplay" controls="controls"></video>
      #
      # @example Fallback Content
      #
      #   <%=
      #     video("movie.mp4") do
      #       "Your browser does not support the video tag"
      #     end
      #   %>
      #
      #   # <video src="/assets/movie.mp4">
      #   #  Your browser does not support the video tag
      #   # </video>
      #
      # @example Tracks
      #
      #   <%=
      #     video("movie.mp4") do
      #       tag.track(kind: "captions", src: asset_url("movie.en.vtt"),
      #             srclang: "en", label: "English")
      #     end
      #   %>
      #
      #   # <video src="/assets/movie.mp4">
      #   #   <track kind="captions" src="/assets/movie.en.vtt" srclang="en" label="English">
      #   # </video>
      #
      # @example Without Any Argument
      #
      #   <%= video_tag %>
      #
      #   # ArgumentError
      #
      # @example Without src And Without Block
      #
      #   <%= video_tag(content: true) %>
      #
      #   # ArgumentError
      #
      # @example Fingerprint Mode
      #
      #   <%= video_tag "movie.mp4" %>
      #
      #   # <video src="/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
      #
      # @example CDN Mode
      #
      #   <%= video_tag "movie.mp4" %>
      #
      #   # <video src="https://assets.bookshelf.org/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
      def video_tag(source = nil, options = {}, &blk)
        options = _source_options(source, options, &blk)
        tag.video(**options, &blk)
      end

      # Generate `audio` tag for given source
      #
      # It accepts one string representing the name of the asset, if it comes
      # from the application or third party gems. It also accepts strings
      # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
      #
      # Alternatively, it accepts a block that allows to specify one or more
      # sources via the `source` tag.
      #
      # If the "fingerprint mode" is on, `src` is the fingerprinted
      # version of the relative URL.
      #
      # If the "CDN mode" is on, the `src` is an absolute URL of the
      # application CDN.
      #
      # @param source [String, #url] asset name, absolute URL or asset object
      # @param options [Hash] HTML 5 attributes
      #
      # @return [Hanami::View::HTML::SafeString] the markup
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the audio file is missing
      # from the manifest
      #
      # @raise [ArgumentError] if source isn"t specified both as argument or
      #   tag inside the given block
      #
      # @since 2.1.0
      #
      # @see Hanami::Assets::Helpers#path
      #
      # @example Basic Usage
      #
      #   <%= audio_tag "song.ogg" %>
      #
      #   # <audio src="/assets/song.ogg"></audio>
      #
      # @example Absolute URL
      #
      #   <%= audio_tag "https://example-cdn.com/assets/song.ogg" %>
      #
      #   # <audio src="https://example-cdn.com/assets/song.ogg"></audio>
      #
      # @example Custom HTML Attributes
      #
      #   <%= audio_tag("song.ogg", autoplay: true, controls: true) %>
      #
      #   # <audio src="/assets/song.ogg" autoplay="autoplay" controls="controls"></audio>
      #
      # @example Fallback Content
      #
      #   <%=
      #     audio("song.ogg") do
      #       "Your browser does not support the audio tag"
      #     end
      #   %>
      #
      #   # <audio src="/assets/song.ogg">
      #   #  Your browser does not support the audio tag
      #   # </audio>
      #
      # @example Tracks
      #
      #   <%=
      #     audio("song.ogg") do
      #       tag.track(kind: "captions", src: asset_url("song.pt-BR.vtt"),
      #             srclang: "pt-BR", label: "Portuguese")
      #     end
      #   %>
      #
      #   # <audio src="/assets/song.ogg">
      #   #   <track kind="captions" src="/assets/song.pt-BR.vtt" srclang="pt-BR" label="Portuguese">
      #   # </audio>
      #
      # @example Without Any Argument
      #
      #   <%= audio_tag %>
      #
      #   # ArgumentError
      #
      # @example Without src And Without Block
      #
      #   <%= audio_tag(controls: true) %>
      #
      #   # ArgumentError
      #
      # @example Fingerprint Mode
      #
      #   <%= audio_tag "song.ogg" %>
      #
      #   # <audio src="/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
      #
      # @example CDN Mode
      #
      #   <%= audio_tag "song.ogg" %>
      #
      #   # <audio src="https://assets.bookshelf.org/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
      def audio_tag(source = nil, options = {}, &blk)
        options = _source_options(source, options, &blk)
        tag.audio(**options, &blk)
      end

      # It generates the relative or absolute URL for the given asset.
      # It automatically decides if it has to use the relative or absolute
      # depending on the configuration and current environment.
      #
      # Absolute URLs are returned as they are.
      #
      # It can be the name of the asset, coming from the sources or third party
      # gems.
      #
      # If Fingerprint mode is on, it returns the fingerprinted path of the source
      #
      # If CDN mode is on, it returns the absolute URL of the asset.
      #
      # @param source [String, #url] the asset name or asset object
      #
      # @return [String] the asset path
      #
      # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
      # `subresource_integrity` modes are on and the asset is missing
      # from the manifest
      #
      # @since 2.1.0
      #
      # @example Basic Usage
      #
      #   <%= asset_url "application.js" %>
      #
      #   # "/assets/application.js"
      #
      # @example Alias
      #
      #   <%= asset_url "application.js" %>
      #
      #   # "/assets/application.js"
      #
      # @example Absolute URL
      #
      #   <%= asset_url "https://code.jquery.com/jquery-2.1.4.min.js" %>
      #
      #   # "https://code.jquery.com/jquery-2.1.4.min.js"
      #
      # @example Fingerprint Mode
      #
      #   <%= asset_url "application.js" %>
      #
      #   # "/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
      #
      # @example CDN Mode
      #
      #   <%= asset_url "application.js" %>
      #
      #   # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
      def asset_url(source)
        return source.url if source.respond_to?(:url)
        return source if _absolute_url?(source)

        _context.assets[source].url
      end

      # Random per request nonce value for Content Security Policy (CSP) rules.
      #
      # If the `Hanami::Middleware::ContentSecurityPolicyNonce` middleware is
      # in use, this helper returns the nonce value for the current request
      # or `nil` otherwise.
      #
      # For this policy to work in the browser, you have to add the `'nonce'`
      # placeholder to the script and/or style source policy rule. It will be
      # substituted by the current nonce value like `'nonce-A12OggyZ'.
      #
      # @return [String, nil] nonce value of the current request
      #
      # @since 2.3.0
      #
      # @example App configuration
      #
      #   config.middleware.use Hanami::Middleware::ContentSecurityPolicyNonce
      #   config.actions.content_security_policy[:script_src] = "'self' 'nonce'"
      #   config.actions.content_security_policy[:style_src] = "'self' 'nonce'"
      #
      # @example View helper
      #
      #

      def _safe_tags(*sources, &blk)
        ::Hanami::View::HTML::SafeString.new(
          sources.map(&blk).join(NEW_LINE_SEPARATOR)
        )
      end

      def _typed_url(source, ext)
        source = "#{source}#{ext}" if source.is_a?(String) && _append_extension?(source, ext)
        asset_url(source)
      end

      def _nonce(source, nonce_option)
        if nonce_option == false
          nil
        elsif nonce_option == true || (nonce_option.nil? && !_absolute_url?(source))
          content_security_policy_nonce
        else
          nonce_option
        end
      end

      def _subresource_integrity_value(source, ext)
        return if _absolute_url?(source)

        source = "#{source}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source)
        _context.assets[source].sri
      end


def stylesheet_tag(*sources, **options)
        options = options.reject { |k, _| k.to_sym == :href }
        nonce_option = options.delete(:nonce)

        _safe_tags(*sources) do |source|
          attributes = {
            href: _typed_url(source, STYLESHEET_EXT),
            type: STYLESHEET_MIME_TYPE,
            rel: STYLESHEET_REL,
            nonce: _nonce(source, nonce_option)
          }
          attributes.merge!(options)

          if _context.assets.subresource_integrity? || attributes.include?(:integrity)
            attributes[:integrity] ||= _subresource_integrity_value(source, STYLESHEET_EXT)
            attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
          end

          tag.link(**attributes).to_s
        end
      end


def favicon_tag(source = DEFAULT_FAVICON, options = {})
        options = options.reject { |k, _| k.to_sym == :href }

        attributes = {
          href: asset_url(source),
          rel: FAVICON_REL,
          type: FAVICON_MIME_TYPE
        }
        attributes.merge!(options)

        tag.link(**attributes)
      end

      def asset_url(source)
        return source.url if source.respond_to?(:url)
        return source if _absolute_url?(source)

        _context.assets[source].url
      end


# frozen_string_literal: true

module Hanami
  class Config
    class Actions
      # Config for Content Security Policy in Hanami apps
      #
      # @since 2.0.0
      class ContentSecurityPolicy
        # @since 2.0.0
        # @api private
        def initialize(&blk)
          @policy = {
            base_uri: "'self'",
            child_src: "'self'",
            connect_src: "'self'",
            default_src: "'none'",
            font_src: "'self'",
            form_action: "'self'",
            frame_ancestors: "'self'",
            frame_src: "'self'",
            img_src: "'self' https: data:",
            media_src: "'self'",
            object_src: "'none'",
            script_src: "'self'",
            style_src: "'self' 'unsafe-inline' https:"
          }

          blk&.(self)
        end

        # @since 2.0.0
        # @api private
        def initialize_copy(original_object)
          @policy = original_object.instance_variable_get(:@policy).dup
          super
        end

        # Get a CSP setting
        #
        # @param key [Symbol] the underscored name of the CPS setting
        # @return [String,NilClass] the CSP setting, if any
        #
        # @since 2.0.0
        # @api public
        #
        # @example
        #   module MyApp
        #     class App < Hanami::App
        #       config.actions.content_security_policy[:base_uri] # => "'self'"
        #     end
        #   end
        def [](key)
          @policy[key]
        end

        # Set a CSP setting
        #
        # @param key [Symbol] the underscored name of the CPS setting
        # @param value [String] the CSP setting value
        #
        # @since 2.0.0
        # @api public
        #
        # @example Replace a default value
        #   module MyApp
        #     class App < Hanami::App
        #       config.actions.content_security_policy[:plugin_types] = nil
        #     end
        #   end
        #
        # @example Append to a default value
        #   module MyApp
        #     class App < Hanami::App
        #       config.actions.content_security_policy[:script_src] += " https://my.cdn.test"
        #     end
        #   end
        def []=(key, value)
          @policy[key] = value
        end

        # Deletes a CSP key
        #
        # @param key [Symbol] the underscored name of the CPS setting
        #
        # @since 2.0.0
        # @api public
        #
        # @example
        #   module MyApp
        #     class App < Hanami::App
        #       config.actions.content_security_policy.delete(:object_src)
        #     end
        #   end
        def delete(key)
          @policy.delete(key)
        end

        # Returns true if 'nonce' is used in any of the policies.
        #
        # @return [Boolean]
        #
        # @api public
        # @since 2.3.0
        def nonce?
          @policy.any? { _2.match?(/'nonce'/) }
        end

        # Returns an array of middleware name to support 'nonce' in
        # policies, or an empty array if 'nonce' is not used.
        #
        # @return [Array<(Symbol, Array)>]
        #
        # @api public
        # @since 2.3.0
        def middleware
          return [] unless nonce?

          [Hanami::Middleware::ContentSecurityPolicyNonce]
        end

        # @since 2.0.0
        # @api private
        def to_s
          @policy.map do |key, value|
            "#{dasherize(key)} #{value}"
          end.join(";")
        end

        private

        # @since 2.0.0
        # @api private
        def dasherize(key)
          key.to_s.gsub("_", "-")
        end
      end
    end
  end
end


# frozen_string_literal: true

module Hanami
  module Providers
    class I18n < Hanami::Provider::Source
      # A wrapper that provides a full `I18n`-like interface for an individual I18n backend. This
      # allows each Hanami slice to have its own isolated I18n instance.
      #
      # Unlike the global I18n module which uses class variables for configuration,
      # this wrapper maintains per-instance state for true isolation between slices.
      #
      # @api public
      # @since x.x.x
      class Backend
        attr_reader :backend

        # Returns the default locale.
        #
        # @return [Symbol] the default locale
        #
        # @api public
        # @since x.x.x
        attr_reader :default_locale

        # Creates a new Backend instance.
        #
        # @param backend [I18n::Backend::Base] the underlying I18n backend
        # @param locale [Symbol, String, nil] initial locale to set in thread-local storage
        # @param default_locale [Symbol, String] the default locale to use when no locale is set
        # @param available_locales [Array<Symbol, String>] list of available locales
        # @param fallbacks [I18n::Locale::Fallbacks, nil] fallbacks configuration for missing translations
        #
        # @api private
        # @since x.x.x
        def initialize(backend, locale: nil, default_locale: :en, available_locales: [], fallbacks: nil)
          @backend = backend
          @default_locale = default_locale.to_sym
          @available_locales = Array(available_locales).map(&:to_sym)
          @fallbacks = fallbacks

          # Set initial locale (if provided) in thread-local storage.
          @storage_key = :"hanami_i18n_#{object_id}"
          self.locale = locale if locale
        end

        # rubocop:disable Metrics/PerceivedComplexity

        # Translates the given key.
        #
        # @param key [String, Symbol] the translation key to look up
        # @param options [Hash] translation options
        # @option options [Symbol, String] :locale the locale to use (defaults to current locale)
        # @option options [String, Proc, Array] :default default value if translation is missing
        # @option options [Hash] :scope additional scope for the key
        # @option options [Integer] :count for pluralization
        # @option options [Boolean] :raise whether to raise an exception for missing translations
        #
        # @return [String, Object] the translated string or default value
        #
        # @raise [I18n::MissingTranslationData] if translation is missing and :raise option is true
        #
        # @example Basic translation
        #   translate("hello") # => "Hello"
        #
        # @example Translation with interpolation
        #   translate("greeting", name: "Alice") # => "Hello, Alice"
        #
        # @example With explicit locale
        #   translate("hello", locale: :fr) # => "Bonjour"
        #
        # @api public
        # @since x.x.x
        def translate(key, **options)
          locale = options[:locale] || self.locale

          result = catch(:exception) do
            @backend.translate(locale, key, options)
          end

          # If translation is missing and fallbacks are configured, try fallback locales
          if result.is_a?(::I18n::MissingTranslation) && @fallbacks && !options[:fallback]
            fallback_locales = @fallbacks[locale] - [locale]
            fallback_locales.each do |fallback_locale|
              fallback_result = catch(:exception) do
                @backend.translate(fallback_locale, key, options.merge(fallback: true))
              end

              unless fallback_result.is_a?(::I18n::MissingTranslation)
                return fallback_result
              end
            end
          end

          if result.is_a?(::I18n::MissingTranslation)
            if options[:raise]
              raise ::I18n::MissingTranslationData.new(locale, key, options)
            else
              handle_missing_translation(result, options)
            end
          else
            result
          end
        end

        # rubocop:enable Metrics/PerceivedComplexity

        # @api public
        # @since x.x.x
        alias_method :t, :translate

        # Translates the given key, raising an exception if translation is missing.
        #
        # @param key [String, Symbol] the translation key to look up
        # @param options [Hash] translation options (see {#translate})
        #
        # @return [String, Object] the translated string
        #
        # @raise [I18n::MissingTranslationData] if translation is missing
        #
        # @example
        #   t!("hello") # => "Hello"
        #   t!("missing.key") # raises I18n::MissingTranslationData
        #
        # @api public
        # @since x.x.x
        def t!(key, **options)
          translate(key, **options.merge(raise: true))
        end

        # Localizes the given object (e.g., date, time, number).
        #
        # @param object [Date, Time, DateTime, Numeric] the object to localize
        # @param locale [Symbol, String, nil] the locale to use (defaults to current locale)
        # @param format [Symbol, String, nil] the format to use for localization
        # @param options [Hash] additional localization options
        #
        # @return [String] the localized string representation
        #
        # @example Localize a date
        #   localize(Date.today, format: :long) # => "January 19, 2026"
        #
        # @example Localize with specific locale
        #   localize(Date.today, locale: :fr, format: :long) # => "19 janvier 2026"
        #
        # @api public
        # @since x.x.x
        def localize(object, locale: nil, format: nil, **options)
          locale ||= self.locale
          @backend.localize(locale, object, format, options)
        end

        # @api public
        # @since x.x.x
        alias_method :l, :localize

        # Returns true if a translation exists for the given key.
        #
        # @param key [String, Symbol] the translation key to check
        # @param locale [Symbol, String, nil] the locale to check (defaults to current locale)
        # @param options [Hash] additional options
        #
        # @return [Boolean] true if the translation exists, false otherwise
        #
        # @example
        #   exists?("hello") # => true
        #   exists?("missing.key") # => false
        #
        # @api public
        # @since x.x.x
        def exists?(key, locale: nil, **options)
          locale ||= self.locale
          @backend.exists?(locale, key, options)
        end

        # Transliterates the given string.
        #
        # @param key [String] the string to transliterate
        # @param locale [Symbol, String, nil] the locale to use (defaults to current locale)
        # @param replacement [String, nil] replacement string for non-transliteratable characters
        # @param options [Hash] additional transliteration options
        #
        # @return [String] the transliterated string
        #
        # @example
        #   transliterate("Ærøskøbing") # => "AEroskobing"
        #
        # @api public
        # @since x.x.x
        def transliterate(key, locale: nil, replacement: nil, **options)
          locale ||= self.locale
          @backend.transliterate(locale, key, replacement)
        end

        # Returns available locales.
        #
        # If configured via `config.i18n.available_locales`, returns the configured locales.
        # Otherwise, returns all locales detected from loaded translation files.
        #
        # @api public
        # @since x.x.x
        def available_locales
          if @available_locales.any?
            @available_locales
          else
            @backend.available_locales
          end
        end

        # Reloads translations from translation files.
        #
        # @return [void]
        #
        # @api public
        # @since x.x.x
        def reload!
          @backend.reload!
        end

        # Eager loads translations if the backend supports it.
        #
        # @return [void]
        #
        # @api public
        # @since x.x.x
        def eager_load!
          @backend.eager_load! if @backend.respond_to?(:eager_load!)
        end

        # Returns the current locale from fiber/thread-local storage, or default_locale.
        #
        # This is thread-safe and works correctly with concurrent requests.
        # Each backend instance maintains its own locale in thread storage.
        #
        # @return [Symbol] the current locale or default locale if none is set
        #
        # @example
        #   locale # => :en
        #   self.locale = :fr
        #   locale # => :fr
        #
        # @api public
        # @since x.x.x
        def locale
          Thread.current[@storage_key] || default_locale
        end

        # Sets the current locale in fiber/thread-local storage.
        #
        # This is thread-safe and works correctly with concurrent requests.
        # Each backend instance maintains its own locale in thread storage.
        #
        # @param value [Symbol, String, nil] the locale to set (converted to symbol)
        #
        # @return [Symbol, nil] the locale value that was set
        #
        # @example
        #   self.locale = :fr
        #   self.locale = "de"
        #   self.locale = nil # resets to default_locale
        #
        # @api public
        # @since x.x.x
        def locale=(value)
          Thread.current[@storage_key] = value&.to_sym
        end

        # Executes a block with a temporary locale.
        #
        # This is useful for executing code with a specific locale without affecting
        # other concurrent requests. The previous locale is restored even if the block raises.
        #
        # @param tmp_locale [Symbol, String, nil] the temporary locale to use
        #
        # @yieldreturn [Object] the return value of the block
        #
        # @return [Object] the return value of the block
        #
        # @example
        #   with_locale(:fr) do
        #     t("greeting") # Uses French locale
        #   end
        #   # locale is restored to previous value
        #
        # @example Nested usage
        #   with_locale(:fr) do
        #     with_locale(:de) do
        #       t("hello") # Uses German
        #     end
        #     t("hello") # Uses French
        #   end
        #
        # @api public
        # @since x.x.x
        def with_locale(tmp_locale)
          previous_locale = Thread.current[@storage_key]
          Thread.current[@storage_key] = tmp_locale&.to_sym
          yield
        ensure
          Thread.current[@storage_key] = previous_locale
        end

        private

        def handle_missing_translation(missing_translation, options)
          default = options[:default]
          return default if default.is_a?(String)

          key = missing_translation.key
          key.to_s
        end
      end
    end
  end
end

translate

translate

!(result.is_a?(::I18n::MissingTranslation) && @fallbacks && !options[:fallback])