lib/dry/schema/messages/yaml.rb in dry-schema-1.8.0 vs lib/dry/schema/messages/yaml.rb in dry-schema-1.9.0

- old
+ new

@@ -7,198 +7,205 @@ require "dry/schema/constants" require "dry/schema/messages/abstract" module Dry module Schema - # Plain YAML message backend - # - # @api public - class Messages::YAML < Messages::Abstract - LOCALE_TOKEN = "%<locale>s" - TOKEN_REGEXP = /%{(\w*)}/.freeze - EMPTY_CONTEXT = Object.new.tap { |ctx| - def ctx.context - binding - end - }.freeze.context + module Messages + # Plain YAML message backend + # + # @api public + class YAML < Abstract + LOCALE_TOKEN = "%<locale>s" + TOKEN_REGEXP = /%{(\w*)}/.freeze + EMPTY_CONTEXT = Object.new.tap { |ctx| + def ctx.context + binding + end + }.freeze.context - include Dry::Equalizer(:data) + include ::Dry::Equalizer(:data) - # Loaded localized message templates - # - # @return [Hash] - attr_reader :data + # Loaded localized message templates + # + # @return [Hash] + attr_reader :data - # Translation function - # - # @return [Proc] - attr_reader :t + # Translation function + # + # @return [Proc] + attr_reader :t - # @api private - def self.build(options = EMPTY_HASH) - super do |config| - config.default_locale = :en unless config.default_locale + # @api private + def self.build(options = EMPTY_HASH) + super do |config| + config.default_locale = :en unless config.default_locale - config.root = "%<locale>s.#{config.root}" + config.root = "%<locale>s.#{config.root}" - config.rule_lookup_paths = config.rule_lookup_paths.map { |path| - "%<locale>s.#{path}" - } + config.rule_lookup_paths = config.rule_lookup_paths.map { |path| + "%<locale>s.#{path}" + } + end end - end - # @api private - def self.flat_hash(hash, path = [], keys = {}) - hash.each do |key, value| - flat_hash(value, [*path, key], keys) if value.is_a?(Hash) + # @api private + # rubocop: disable Metrics/PerceivedComplexity + def self.flat_hash(hash, path = EMPTY_ARRAY, keys = {}) + hash.each do |key, value| + flat_hash(value, [*path, key], keys) if value.is_a?(Hash) - if value.is_a?(String) && hash["text"] != value - keys[[*path, key].join(DOT)] = { - text: value, - meta: EMPTY_HASH - } - elsif value.is_a?(Hash) && value["text"].is_a?(String) - keys[[*path, key].join(DOT)] = { - text: value["text"], - meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h - } + if value.is_a?(String) && hash["text"] != value + keys[[*path, key].join(DOT)] = { + text: value, + meta: EMPTY_HASH + } + elsif value.is_a?(Hash) && value["text"].is_a?(String) + keys[[*path, key].join(DOT)] = { + text: value["text"], + meta: value.reject { _1.eql?("text") }.transform_keys(&:to_sym) + } + end end + + keys end - keys - end + # rubocop: enable Metrics/PerceivedComplexity - # @api private - def self.cache - @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new } - end + # @api private + def self.cache + @cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new } + end - # @api private - def self.source_cache - @source_cache ||= Concurrent::Map.new - end + # @api private + def self.source_cache + @source_cache ||= Concurrent::Map.new + end - # @api private - def initialize(data: EMPTY_HASH, config: nil) - super() - @data = data - @config = config if config - @t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) } - end + # @api private + def initialize(data: EMPTY_HASH, config: nil) + super() + @data = data + @config = config if config + @t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) } + end - # Get an array of looked up paths - # - # @param [Symbol] predicate - # @param [Hash] options - # - # @return [String] - # - # @api public - def looked_up_paths(predicate, options) - super.map { |path| path % {locale: options[:locale] || default_locale} } - end + # Get an array of looked up paths + # + # @param [Symbol] predicate + # @param [Hash] options + # + # @return [String] + # + # @api public + def looked_up_paths(predicate, options) + super.map { |path| path % {locale: options[:locale] || default_locale} } + end - # Get a message for the given key and its options - # - # @param [Symbol] key - # @param [Hash] options - # - # @return [String] - # - # @api public - def get(key, options = EMPTY_HASH) - data[evaluated_key(key, options)] - end + # Get a message for the given key and its options + # + # @param [Symbol] key + # @param [Hash] options + # + # @return [String] + # + # @api public + def get(key, options = EMPTY_HASH) + data[evaluated_key(key, options)] + end - # Check if given key is defined - # - # @return [Boolean] - # - # @api public - def key?(key, options = EMPTY_HASH) - data.key?(evaluated_key(key, options)) - end + # Check if given key is defined + # + # @return [Boolean] + # + # @api public + def key?(key, options = EMPTY_HASH) + data.key?(evaluated_key(key, options)) + end - # Merge messages from an additional path - # - # @param [String] overrides - # - # @return [Messages::I18n] - # - # @api public - def merge(overrides) - if overrides.is_a?(Hash) - self.class.new( - data: data.merge(self.class.flat_hash(overrides)), - config: config - ) - else - self.class.new( - data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) }, - config: config - ) + # Merge messages from an additional path + # + # @param [String] overrides + # + # @return [Messages::I18n] + # + # @api public + def merge(overrides) + if overrides.is_a?(Hash) + self.class.new( + data: data.merge(self.class.flat_hash(overrides)), + config: config + ) + else + self.class.new( + data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) }, + config: config + ) + end end - end - # @api private - def prepare - @data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge) - self - end + # @api private + def prepare + @data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge) + self + end - # @api private - def interpolatable_data(key, options, **data) - tokens = evaluation_context(key, options).fetch(:tokens) - data.select { |k,| tokens.include?(k) } - end + # @api private + def interpolatable_data(key, options, **data) + tokens = evaluation_context(key, options).fetch(:tokens) + data.select { |k,| tokens.include?(k) } + end - # @api private - def interpolate(key, options, **data) - evaluator = evaluation_context(key, options).fetch(:evaluator) - data.empty? ? evaluator.() : evaluator.(**data) - end + # @api private + def interpolate(key, options, **data) + evaluator = evaluation_context(key, options).fetch(:evaluator) + data.empty? ? evaluator.() : evaluator.(**data) + end - private + private - # @api private - def evaluation_context(key, options) - cache.fetch_or_store(get(key, options).fetch(:text)) do |input| - tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set - text = input.gsub("%", "#") + # @api private + def evaluation_context(key, options) + cache.fetch_or_store(get(key, options).fetch(:text)) do |input| + tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set + text = input.gsub("%", "#") - # rubocop:disable Security/Eval - evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1) - -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" } - RUBY - # rubocop:enable Security/Eval + # rubocop:disable Security/Eval + # rubocop:disable Style/DocumentDynamicEvalDefinition + evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1) + -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" } + RUBY + # rubocop:enable Style/DocumentDynamicEvalDefinition + # rubocop:enable Security/Eval - { - tokens: tokens, - evaluator: evaluator - } + { + tokens: tokens, + evaluator: evaluator + } + end end - end - # @api private - def cache - @cache ||= self.class.cache[self] - end - - # @api private - def load_translations(path) - data = self.class.source_cache.fetch_or_store(path) do - self.class.flat_hash(YAML.load_file(path)).freeze + # @api private + def cache + @cache ||= self.class.cache[self] end - return data unless custom_top_namespace?(path) + # @api private + def load_translations(path) + data = self.class.source_cache.fetch_or_store(path) do + self.class.flat_hash(::YAML.load_file(path)).freeze + end - data.map { |k, v| [k.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace), v] }.to_h - end + return data unless custom_top_namespace?(path) - # @api private - def evaluated_key(key, options) - return key unless key.include?(LOCALE_TOKEN) + data.transform_keys { _1.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace) } + end - key % {locale: options[:locale] || default_locale} + # @api private + def evaluated_key(key, options) + return key unless key.include?(LOCALE_TOKEN) + + key % {locale: options[:locale] || default_locale} + end end end end end