lib/dry/schema/messages/abstract.rb
# frozen_string_literal: true
require 'set'
require 'concurrent/map'
require 'dry/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, nil
setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
setting :top_namespace, DEFAULT_MESSAGES_ROOT
setting :root, 'errors'
setting :lookup_options, %i[root predicate path val_type arg_type].freeze
setting :lookup_paths, [
'%<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, ['rules.%<name>s'].freeze
setting :arg_types, Hash.new { |*| 'default' }.update(
Range => 'range'
)
setting :val_types, Hash.new { |*| 'default' }.update(
Range => 'range',
String => 'string'
)
# @api private
def self.cache
@cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
end
# @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)
cache.fetch_or_store([predicate, options.reject { |k,| k.equal?(:input) }]) do
text, meta = lookup(predicate, options)
[Template[text], meta] if text
end
end
alias_method :[], :call
# Try to find a message for the given predicate and its options
#
# @api private
#
# rubocop:disable Metrics/AbcSize
def lookup(predicate, options)
tokens = 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
)
opts = options.reject { |k, _| config.lookup_options.include?(k) }
path = lookup_paths(tokens).detect { |key| key?(key, opts) }
return unless path
text = get(path, opts)
if text.is_a?(Hash)
text.values_at(:text, :meta)
else
[text, EMPTY_HASH]
end
end
# rubocop:enable Metrics/AbcSize
# @api private
def 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 cache
@cache ||= self.class.cache[self]
end
# @api private
def default_locale
config.default_locale
end
private
# @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