lib/dry/schema/messages/yaml.rb in dry-schema-1.4.3 vs lib/dry/schema/messages/yaml.rb in dry-schema-1.5.0

- old
+ new

@@ -1,21 +1,27 @@ # frozen_string_literal: true -require 'yaml' -require 'pathname' +require "yaml" +require "pathname" -require 'dry/equalizer' -require 'dry/schema/constants' -require 'dry/schema/messages/abstract' +require "dry/equalizer" +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' + 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) # Loaded localized message templates # @@ -43,26 +49,31 @@ # @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) - if value.is_a?(String) && hash['text'] != value + 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) + 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 + text: value["text"], + meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h } end end keys end # @api private + def self.cache + @cache ||= Concurrent::Map.new { |h, k| h[k] = 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) } @@ -75,11 +86,11 @@ # # @return [String] # # @api public def looked_up_paths(predicate, options) - super.map { |path| path % { locale: options[:locale] || default_locale } } + super.map { |path| path % {locale: options[:locale] || default_locale} } end # Get a message for the given key and its options # # @param [Symbol] key @@ -122,17 +133,53 @@ end end # @api private def prepare - @data = config.load_paths.map { |path| load_translations(path) }.reduce(:merge) + @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 interpolate(key, options, **data) + evaluator = evaluation_context(key, options).fetch(:evaluator) + data.empty? ? evaluator.() : evaluator.(**data) + end + 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("%", "#") + + # rubocop:disable Security/Eval + evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1) + -> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" } + RUBY + # rubocop:enable Security/Eval + + { + tokens: tokens, + evaluator: evaluator + } + end + end + + # @api private + def cache + @cache ||= self.class.cache[self] + end + + # @api private def load_translations(path) data = self.class.flat_hash(YAML.load_file(path)) return data unless custom_top_namespace?(path) @@ -141,10 +188,10 @@ # @api private def evaluated_key(key, options) return key unless key.include?(LOCALE_TOKEN) - key % { locale: options[:locale] || default_locale } + key % {locale: options[:locale] || default_locale} end end end end