dry-schema 1.3.2 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,7 @@ module Dry
41
41
  attr_reader :default_lookup_options
42
42
 
43
43
  # @api private
44
- def initialize(messages, options = EMPTY_HASH)
44
+ def initialize(messages, **options)
45
45
  super
46
46
  @options = options
47
47
  @default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
@@ -55,7 +55,7 @@ module Dry
55
55
 
56
56
  return self if updated_opts.eql?(options)
57
57
 
58
- self.class.new(messages, updated_opts)
58
+ self.class.new(messages, **updated_opts)
59
59
  end
60
60
 
61
61
  # @api private
@@ -105,7 +105,7 @@ module Dry
105
105
  left, right = node.map { |n| visit(n, opts) }
106
106
 
107
107
  if [left, right].flatten.map(&:path).uniq.size == 1
108
- Message::Or.new(left, right, proc { |k| messages.translate(k, default_lookup_options) })
108
+ Message::Or.new(left, right, proc { |k| messages.translate(k, **default_lookup_options) })
109
109
  elsif right.is_a?(Array)
110
110
  right
111
111
  else
@@ -116,7 +116,7 @@ module Dry
116
116
  # @api private
117
117
  def visit_namespace(node, opts)
118
118
  ns, rest = node
119
- self.class.new(messages.namespaced(ns), options).visit(rest, opts)
119
+ self.class.new(messages.namespaced(ns), **options).visit(rest, opts)
120
120
  end
121
121
 
122
122
  # @api private
@@ -130,7 +130,8 @@ module Dry
130
130
  path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
131
131
  ).to_h
132
132
 
133
- template, meta = messages[predicate, options] || raise(MissingMessageError, path)
133
+ template, meta = messages[predicate, options] ||
134
+ raise(MissingMessageError.new(path, messages.looked_up_paths(predicate, options)))
134
135
 
135
136
  text = message_text(template, tokens, options)
136
137
 
@@ -92,45 +92,49 @@ module Dry
92
92
  #
93
93
  # @api public
94
94
  def call(predicate, options)
95
- cache.fetch_or_store([predicate, options.reject { |k,| k.equal?(:input) }]) do
95
+ cache.fetch_or_store(cache_key(predicate, options)) do
96
96
  text, meta = lookup(predicate, options)
97
97
  [Template[text], meta] if text
98
98
  end
99
99
  end
100
100
  alias_method :[], :call
101
101
 
102
+ # Retrieve an array of looked up paths
103
+ #
104
+ # @param [Symbol] predicate
105
+ # @param [Hash] options
106
+ #
107
+ # @return [String]
108
+ #
109
+ # @api public
110
+ def looked_up_paths(predicate, options)
111
+ tokens = lookup_tokens(predicate, options)
112
+ filled_lookup_paths(tokens)
113
+ end
114
+
102
115
  # Try to find a message for the given predicate and its options
103
116
  #
104
117
  # @api private
105
118
  #
106
119
  # rubocop:disable Metrics/AbcSize
107
120
  def lookup(predicate, options)
108
- tokens = options.merge(
109
- predicate: predicate,
110
- root: options[:not] ? "#{root}.not" : root,
111
- arg_type: config.arg_types[options[:arg_type]],
112
- val_type: config.val_types[options[:val_type]],
113
- message_type: options[:message_type] || :failure
114
- )
115
-
116
121
  opts = options.reject { |k, _| config.lookup_options.include?(k) }
117
-
118
- path = lookup_paths(tokens).detect { |key| key?(key, opts) }
122
+ path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
119
123
 
120
124
  return unless path
121
125
 
122
- text = get(path, opts)
123
-
124
- if text.is_a?(Hash)
125
- text.values_at(:text, :meta)
126
- else
127
- [text, EMPTY_HASH]
128
- end
126
+ get(path, opts).values_at(:text, :meta)
129
127
  end
130
128
  # rubocop:enable Metrics/AbcSize
131
129
 
132
130
  # @api private
133
- def lookup_paths(tokens)
131
+ def lookup_paths(predicate, options)
132
+ tokens = lookup_tokens(predicate, options)
133
+ filled_lookup_paths(tokens)
134
+ end
135
+
136
+ # @api private
137
+ def filled_lookup_paths(tokens)
134
138
  config.lookup_paths.map { |path| path % tokens }
135
139
  end
136
140
 
@@ -167,8 +171,28 @@ module Dry
167
171
  config.default_locale
168
172
  end
169
173
 
174
+ # @api private
175
+ def cache_key(predicate, options)
176
+ if options.key?(:input)
177
+ [predicate, options.reject { |k,| k.equal?(:input) }]
178
+ else
179
+ [predicate, options]
180
+ end
181
+ end
182
+
170
183
  private
171
184
 
185
+ # @api private
186
+ def lookup_tokens(predicate, options)
187
+ options.merge(
188
+ predicate: predicate,
189
+ root: options[:not] ? "#{root}.not" : root,
190
+ arg_type: config.arg_types[options[:arg_type]],
191
+ val_type: config.val_types[options[:val_type]],
192
+ message_type: options[:message_type] || :failure
193
+ )
194
+ end
195
+
172
196
  # @api private
173
197
  def custom_top_namespace?(path)
174
198
  path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
@@ -29,7 +29,24 @@ module Dry
29
29
  #
30
30
  # @api public
31
31
  def get(key, options = EMPTY_HASH)
32
- t.(key, locale: options.fetch(:locale, default_locale)) if key
32
+ return unless key
33
+
34
+ opts = { locale: options.fetch(:locale, default_locale) }
35
+
36
+ translation = t.(key, opts)
37
+ text_key = "#{key}.text"
38
+
39
+ if !translation.is_a?(Hash) || !key?(text_key, opts)
40
+ return {
41
+ text: translation,
42
+ meta: EMPTY_HASH
43
+ }
44
+ end
45
+
46
+ {
47
+ text: t.(text_key, opts),
48
+ meta: extract_meta(key, translation, opts)
49
+ }
33
50
  end
34
51
 
35
52
  # Check if given key is defined
@@ -79,6 +96,15 @@ module Dry
79
96
  self
80
97
  end
81
98
 
99
+ # @api private
100
+ def cache_key(predicate, options)
101
+ if options[:locale]
102
+ super
103
+ else
104
+ [*super, I18n.locale]
105
+ end
106
+ end
107
+
82
108
  private
83
109
 
84
110
  # @api private
@@ -91,6 +117,13 @@ module Dry
91
117
  I18n.backend.store_translations(locale, data[locale.to_s])
92
118
  end
93
119
  end
120
+
121
+ def extract_meta(parent_key, translation, options)
122
+ translation.keys.each_with_object({}) do |k, meta|
123
+ meta_key = "#{parent_key}.#{k}"
124
+ meta[k] = t.(meta_key, options) if k != :text && key?(meta_key, options)
125
+ end
126
+ end
94
127
  end
95
128
  end
96
129
  end
@@ -55,7 +55,7 @@ module Dry
55
55
  end
56
56
 
57
57
  # @api private
58
- def lookup_paths(tokens)
58
+ def filled_lookup_paths(tokens)
59
59
  super(tokens.merge(root: "#{tokens[:root]}.#{namespace}")) + super
60
60
  end
61
61
 
@@ -64,6 +64,11 @@ module Dry
64
64
  base_paths = messages.rule_lookup_paths(tokens)
65
65
  base_paths.map { |key| key.gsub('dry_schema', "dry_schema.#{namespace}") } + base_paths
66
66
  end
67
+
68
+ # @api private
69
+ def cache_key(predicate, options)
70
+ messages.cache_key(predicate, options)
71
+ end
67
72
  end
68
73
  end
69
74
  end
@@ -59,7 +59,7 @@ module Dry
59
59
 
60
60
  # @api private
61
61
  def call(data = EMPTY_HASH)
62
- data.empty? ? evaluator.() : evaluator.(data)
62
+ data.empty? ? evaluator.() : evaluator.(**data)
63
63
  end
64
64
  alias_method :[], :call
65
65
  end
@@ -68,6 +68,18 @@ module Dry
68
68
  @t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
69
69
  end
70
70
 
71
+ # Get an array of looked up paths
72
+ #
73
+ # @param [Symbol] predicate
74
+ # @param [Hash] options
75
+ #
76
+ # @return [String]
77
+ #
78
+ # @api public
79
+ def looked_up_paths(predicate, options)
80
+ super.map { |path| path % { locale: options[:locale] || default_locale } }
81
+ end
82
+
71
83
  # Get a message for the given key and its options
72
84
  #
73
85
  # @param [Symbol] key
@@ -29,9 +29,10 @@ module Dry
29
29
  end
30
30
 
31
31
  # @api private
32
- def ast(input=Undefined)
32
+ def ast(input = Undefined)
33
33
  [:namespace, [namespace, rule.ast(input)]]
34
34
  end
35
+ alias_method :to_ast, :ast
35
36
  end
36
37
  end
37
38
  end
@@ -1,35 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/logic/predicates'
4
+ require 'dry/types/predicate_registry'
4
5
 
5
6
  module Dry
6
7
  module Schema
7
8
  # A registry with predicate objects from `Dry::Logic::Predicates`
8
9
  #
9
10
  # @api private
10
- class PredicateRegistry
11
- # @api private
12
- attr_reader :predicates
13
-
14
- # @api private
15
- attr_reader :has_predicate
16
-
17
- # @api private
18
- def initialize(predicates = Dry::Logic::Predicates)
19
- @predicates = predicates
20
- @has_predicate = ::Kernel.instance_method(:respond_to?).bind(@predicates)
21
- end
22
-
23
- # @api private
24
- def [](name)
25
- predicates[name]
26
- end
27
-
28
- # @api private
29
- def key?(name)
30
- has_predicate.(name)
31
- end
32
-
11
+ class PredicateRegistry < Dry::Types::PredicateRegistry
33
12
  # @api private
34
13
  def arg_list(name, *values)
35
14
  predicate = self[name]
@@ -5,6 +5,7 @@ require 'dry/initializer'
5
5
 
6
6
  require 'dry/schema/type_registry'
7
7
  require 'dry/schema/type_container'
8
+ require 'dry/schema/processor_steps'
8
9
  require 'dry/schema/rule_applier'
9
10
  require 'dry/schema/key_coercer'
10
11
  require 'dry/schema/value_coercer'
@@ -12,14 +13,9 @@ require 'dry/schema/value_coercer'
12
13
  module Dry
13
14
  module Schema
14
15
  # Processes input data using objects configured within the DSL
16
+ # Processing is split into steps represented by `ProcessorSteps`.
15
17
  #
16
- # Processing is split into 4 main steps:
17
- #
18
- # 1. Prepare input hash using a key map
19
- # 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
20
- # 3. Apply value coercions based on type specifications
21
- # 4. Apply rules
22
- #
18
+ # @see ProcessorSteps
23
19
  # @see Params
24
20
  # @see JSON
25
21
  #
@@ -29,10 +25,10 @@ module Dry
29
25
  extend Dry::Configurable
30
26
 
31
27
  setting :key_map_type
32
- setting :type_registry_namespace, :nominal
28
+ setting :type_registry_namespace, :strict
33
29
  setting :filter_empty_string, false
34
30
 
35
- option :steps, default: -> { EMPTY_ARRAY.dup }
31
+ option :steps, default: -> { ProcessorSteps.new }
36
32
 
37
33
  option :schema_dsl
38
34
 
@@ -66,7 +62,7 @@ module Dry
66
62
  # @api public
67
63
  def new(options = nil, &block)
68
64
  if options || block
69
- processor = super
65
+ processor = super(**(options || EMPTY_HASH))
70
66
  yield(processor) if block
71
67
  processor
72
68
  elsif definition
@@ -77,16 +73,6 @@ module Dry
77
73
  end
78
74
  end
79
75
 
80
- # Append a step
81
- #
82
- # @return [Processor]
83
- #
84
- # @api private
85
- def <<(step)
86
- steps << step
87
- self
88
- end
89
-
90
76
  # Apply processing steps to the provided input
91
77
  #
92
78
  # @param [Hash] input
@@ -96,10 +82,7 @@ module Dry
96
82
  # @api public
97
83
  def call(input)
98
84
  Result.new(input, message_compiler: message_compiler) do |result|
99
- steps.each do |step|
100
- output = step.(result)
101
- result.replace(output) if output.is_a?(::Hash)
102
- end
85
+ steps.call(result)
103
86
  end
104
87
  end
105
88
  alias_method :[], :call
@@ -119,7 +102,7 @@ module Dry
119
102
  #
120
103
  # @api public
121
104
  def key_map
122
- @key_map ||= steps.detect { |s| s.is_a?(KeyCoercer) }.key_map
105
+ steps[:key_coercer].key_map
123
106
  end
124
107
 
125
108
  # Return string represntation
@@ -139,7 +122,7 @@ module Dry
139
122
  #
140
123
  # @api private
141
124
  def type_schema
142
- @type_schema ||= steps.detect { |s| s.is_a?(ValueCoercer) }.type_schema
125
+ steps[:value_coercer].type_schema
143
126
  end
144
127
 
145
128
  # Return the rules config
@@ -180,7 +163,7 @@ module Dry
180
163
  #
181
164
  # @api private
182
165
  def rule_applier
183
- @rule_applier ||= steps.last
166
+ steps[:rule_applier]
184
167
  end
185
168
  alias_method :to_rule, :rule_applier
186
169
 
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/initializer'
4
+
5
+ module Dry
6
+ module Schema
7
+ # Steps for the Dry::Schema::Processor
8
+ #
9
+ # There are 4 main steps:
10
+ #
11
+ # 1. `key_coercer` - Prepare input hash using a key map
12
+ # 2. `filter_schema` - Apply pre-coercion filtering rules
13
+ # (optional step, used only when `filter` was used)
14
+ # 3. `value_coercer` - Apply value coercions based on type specifications
15
+ # 4. `rule_applier` - Apply rules
16
+ #
17
+ # @see Processor
18
+ #
19
+ # @api public
20
+ class ProcessorSteps
21
+ extend Dry::Initializer
22
+
23
+ STEPS_IN_ORDER = %i[key_coercer filter_schema value_coercer rule_applier].freeze
24
+
25
+ option :steps, default: -> { EMPTY_HASH.dup }
26
+ option :before_steps, default: -> { EMPTY_HASH.dup }
27
+ option :after_steps, default: -> { EMPTY_HASH.dup }
28
+
29
+ # Executes steps and callbacks in order
30
+ #
31
+ # @param [Result] result
32
+ #
33
+ # @return [Result]
34
+ #
35
+ # @api public
36
+ def call(result)
37
+ STEPS_IN_ORDER.each do |name|
38
+ before_steps[name]&.each { |step| process_step(step, result) }
39
+ process_step(steps[name], result)
40
+ after_steps[name]&.each { |step| process_step(step, result) }
41
+ end
42
+ result
43
+ end
44
+
45
+ # Returns step by name
46
+ #
47
+ # @param [Symbol] name The step name
48
+ #
49
+ # @api public
50
+ def [](name)
51
+ steps[name]
52
+ end
53
+
54
+ # Sets step by name
55
+ #
56
+ # @param [Symbol] name The step name
57
+ #
58
+ # @api public
59
+ def []=(name, value)
60
+ validate_step_name(name)
61
+ steps[name] = value
62
+ end
63
+
64
+ # Add passed block before mentioned step
65
+ #
66
+ # @param [Symbol] name The step name
67
+ #
68
+ # @return [ProcessorSteps]
69
+ #
70
+ # @api public
71
+ def after(name, &block)
72
+ validate_step_name(name)
73
+ after_steps[name] ||= EMPTY_ARRAY.dup
74
+ after_steps[name] << block.to_proc
75
+ self
76
+ end
77
+
78
+ # Add passed block before mentioned step
79
+ #
80
+ # @param [Symbol] name The step name
81
+ #
82
+ # @return [ProcessorSteps]
83
+ #
84
+ # @api public
85
+ def before(name, &block)
86
+ validate_step_name(name)
87
+ before_steps[name] ||= EMPTY_ARRAY.dup
88
+ before_steps[name] << block.to_proc
89
+ self
90
+ end
91
+
92
+ # @api private
93
+ def process_step(step, result)
94
+ return unless step
95
+
96
+ output = step.(result)
97
+ result.replace(output) if output.is_a?(::Hash)
98
+ end
99
+
100
+ # @api private
101
+ def validate_step_name(name)
102
+ return if STEPS_IN_ORDER.include?(name)
103
+
104
+ raise ArgumentError, "Undefined step name #{name}. Available names: #{STEPS_IN_ORDER}"
105
+ end
106
+
107
+ # @api private
108
+ def initialize_copy(source)
109
+ super
110
+ @steps = source.steps.dup
111
+ @before_steps = source.before_steps.dup
112
+ @after_steps = source.after_steps.dup
113
+ end
114
+ end
115
+ end
116
+ end