lib/dry/schema/messages/abstract.rb



# frozen_string_literal: true

require "set"
require "concurrent/map"
require "dry/core/equalizer"
require "dry/configurable"

require "dry/schema/constants"
require "dry/schema/messages/template"

module Dry
  module Schema
    module Messages
      # Abstract class for message backends
      #
      # @api public
      class Abstract
        include Dry::Configurable
        include Dry::Equalizer(:config)

        setting :default_locale
        setting :load_paths, default: Set[DEFAULT_MESSAGES_PATH]
        setting :top_namespace, default: DEFAULT_MESSAGES_ROOT
        setting :root, default: "errors"
        setting :lookup_options, default: %i[root predicate path val_type arg_type].freeze

        setting :lookup_paths, default: [
          "%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s",
          "%<root>s.rules.%<path>s.%<predicate>s",
          "%<root>s.%<predicate>s.%<message_type>s",
          "%<root>s.%<predicate>s.value.%<path>s",
          "%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s",
          "%<root>s.%<predicate>s.value.%<val_type>s",
          "%<root>s.%<predicate>s.arg.%<arg_type>s",
          "%<root>s.%<predicate>s"
        ].freeze

        setting :rule_lookup_paths, default: ["rules.%<name>s"].freeze

        setting :arg_types, default: Hash.new { |*| "default" }.update(
          Range => "range"
        )

        setting :val_types, default: Hash.new { |*| "default" }.update(
          Range => "range",
          String => "string"
        )

        # @api private
        def self.build(options = EMPTY_HASH)
          messages = new

          messages.configure do |config|
            options.each do |key, value|
              config.public_send(:"#{key}=", value)
            end

            config.root = "#{config.top_namespace}.#{config.root}"

            config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
              "#{config.top_namespace}.#{path}"
            }

            yield(config) if block_given?
          end

          messages.prepare
        end

        # @api private
        def translate(key, locale: default_locale)
          t["#{config.top_namespace}.#{key}", locale: locale]
        end

        # @api private
        def rule(name, options = {})
          tokens = {name: name, locale: options.fetch(:locale, default_locale)}
          path = rule_lookup_paths(tokens).detect { |key| key?(key, options) }

          rule = get(path, options) if path
          rule.is_a?(Hash) ? rule[:text] : rule
        end

        # Retrieve a message template
        #
        # @return [Template]
        #
        # @api public
        def call(predicate, options)
          options = {locale: default_locale, **options}
          opts = options.reject { |k,| config.lookup_options.include?(k) }
          path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }

          return unless path

          result = get(path, opts)

          [
            Template.new(
              messages: self,
              key: path,
              options: opts
            ),
            result[:meta]
          ]
        end

        alias_method :[], :call

        # Check if given key is defined
        #
        # @return [Boolean]
        #
        # @api public
        def key?(_key, _options = EMPTY_HASH)
          raise NotImplementedError
        end

        # Retrieve an array of looked up paths
        #
        # @param [Symbol] predicate
        # @param [Hash] options
        #
        # @return [String]
        #
        # @api public
        def looked_up_paths(predicate, options)
          tokens = lookup_tokens(predicate, options)
          filled_lookup_paths(tokens)
        end

        # @api private
        def lookup_paths(predicate, options)
          tokens = lookup_tokens(predicate, options)
          filled_lookup_paths(tokens)
        end

        # @api private
        def filled_lookup_paths(tokens)
          config.lookup_paths.map { |path| path % tokens }
        end

        # @api private
        def rule_lookup_paths(tokens)
          config.rule_lookup_paths.map { |key| key % tokens }
        end

        # Return a new message backend that will look for messages under provided namespace
        #
        # @param [Symbol,String] namespace
        #
        # @api public
        def namespaced(namespace)
          Dry::Schema::Messages::Namespaced.new(namespace, self)
        end

        # Return root path to messages file
        #
        # @return [Pathname]
        #
        # @api public
        def root
          config.root
        end

        # @api private
        def default_locale
          config.default_locale
        end

        # @api private
        def interpolatable_data(_key, _options, **_data)
          raise NotImplementedError
        end

        # @api private
        def interpolate(_key, _options, **_data)
          raise NotImplementedError
        end

        private

        # @api private
        def lookup_tokens(predicate, options)
          options.merge(
            predicate: predicate,
            root: options[:not] ? "#{root}.not" : root,
            arg_type: config.arg_types[options[:arg_type]],
            val_type: config.val_types[options[:val_type]],
            message_type: options[:message_type] || :failure
          )
        end

        # @api private
        def custom_top_namespace?(path)
          path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
        end
      end
    end
  end
end