dry-schema 1.3.1 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,10 +26,11 @@ module Dry
26
26
  # An error raised when a localized message cannot be found
27
27
  MissingMessageError = Class.new(StandardError) do
28
28
  # @api private
29
- def initialize(path)
29
+ def initialize(path, paths = [])
30
30
  *rest, rule = path
31
31
  super(<<~STR)
32
- Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found
32
+ Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found. Searched in:
33
+ #{paths.map { |string| "\"#{string}\"" }.join("\n")}
33
34
  STR
34
35
  end
35
36
  end
@@ -9,6 +9,7 @@ require 'dry/schema/types'
9
9
  require 'dry/schema/macros'
10
10
 
11
11
  require 'dry/schema/processor'
12
+ require 'dry/schema/processor_steps'
12
13
  require 'dry/schema/key_map'
13
14
  require 'dry/schema/key_coercer'
14
15
  require 'dry/schema/value_coercer'
@@ -64,18 +65,21 @@ module Dry
64
65
  # @return [Compiler] A key=>type map defined within the DSL
65
66
  option :types, default: -> { EMPTY_HASH.dup }
66
67
 
67
- # @return [DSL] An optional parent DSL object that will be used to merge keys and rules
68
- option :parent, optional: true
68
+ # @return [Array] Optional parent DSL objects, that will be used to merge keys and rules
69
+ option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
69
70
 
70
71
  # @return [Config] Configuration object exposed via `#configure` method
71
72
  option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
72
73
 
74
+ # @return [ProcessorSteps] Steps for the processor
75
+ option :steps, default: proc { parent ? parent.steps.dup : ProcessorSteps.new }
76
+
73
77
  # Build a new DSL object and evaluate provided block
74
78
  #
75
79
  # @param [Hash] options
76
80
  # @option options [Class] :processor The processor type (`Params`, `JSON` or a custom sub-class)
77
81
  # @option options [Compiler] :compiler An instance of a rule compiler (must be compatible with `Schema::Compiler`) (optional)
78
- # @option options [DSL] :parent An instance of the parent DSL (optional)
82
+ # @option options [Array[DSL]] :parent One or more instances of the parent DSL (optional)
79
83
  # @option options [Config] :config A configuration object (optional)
80
84
  #
81
85
  # @see Schema.define
@@ -189,9 +193,10 @@ module Dry
189
193
  #
190
194
  # @api private
191
195
  def call
192
- steps = [key_coercer]
193
- steps << filter_schema.rule_applier if filter_rules?
194
- steps << value_coercer << rule_applier
196
+ steps[:key_coercer] = key_coercer
197
+ steps[:value_coercer] = value_coercer
198
+ steps[:rule_applier] = rule_applier
199
+ steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
195
200
 
196
201
  processor_type.new(schema_dsl: self, steps: steps)
197
202
  end
@@ -215,14 +220,54 @@ module Dry
215
220
  -> member_type { type_registry['array'].of(resolve_type(member_type)) }
216
221
  end
217
222
 
223
+ # Method allows steps injection to the processor
224
+ #
225
+ # @example
226
+ # before(:rule_applier) do |input|
227
+ # input.compact
228
+ # end
229
+ #
230
+ # @return [DSL]
231
+ #
232
+ # @api public
233
+ def before(key, &block)
234
+ steps.before(key, &block)
235
+ self
236
+ end
237
+
238
+ # Method allows steps injection to the processor
239
+ #
240
+ # @example
241
+ # after(:rule_applier) do |input|
242
+ # input.compact
243
+ # end
244
+ #
245
+ # @return [DSL]
246
+ #
247
+ # @api public
248
+ def after(key, &block)
249
+ steps.after(key, &block)
250
+ self
251
+ end
252
+
253
+ # The parent (last from parents) which is used for copying non mergeable configuration
254
+ #
255
+ # @return DSL
256
+ #
257
+ # @api public
258
+ def parent
259
+ @parent ||= parents.last
260
+ end
261
+
218
262
  # Return type schema used by the value coercer
219
263
  #
220
264
  # @return [Dry::Types::Safe]
221
265
  #
222
266
  # @api private
223
267
  def type_schema
224
- schema = type_registry['hash'].schema(types).lax
225
- parent ? parent.type_schema.schema(schema.to_a) : schema
268
+ our_schema = type_registry['hash'].schema(types).lax
269
+ schemas = [*parents.map(&:type_schema), our_schema]
270
+ schemas.inject { |result, schema| result.schema(schema.to_a) }
226
271
  end
227
272
 
228
273
  # Return a new DSL instance using the same processor type
@@ -276,14 +321,18 @@ module Dry
276
321
  #
277
322
  # @api private
278
323
  def filter_schema_dsl
279
- @filter_schema_dsl ||= new(parent: parent_filter_schema)
324
+ @filter_schema_dsl ||= new(parent: parent_filter_schemas)
280
325
  end
281
326
 
282
327
  # Check if any filter rules were defined
283
328
  #
284
329
  # @api private
285
330
  def filter_rules?
286
- (instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?) || parent&.filter_rules?
331
+ if instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?
332
+ return true
333
+ end
334
+
335
+ parents.any?(&:filter_rules?)
287
336
  end
288
337
 
289
338
  protected
@@ -323,10 +372,8 @@ module Dry
323
372
  private
324
373
 
325
374
  # @api private
326
- def parent_filter_schema
327
- return unless parent
328
-
329
- parent.filter_schema if parent.filter_rules?
375
+ def parent_filter_schemas
376
+ parents.select(&:filter_rules?).map(&:filter_schema)
330
377
  end
331
378
 
332
379
  # Build a key coercer
@@ -380,12 +427,12 @@ module Dry
380
427
 
381
428
  # @api private
382
429
  def parent_rules
383
- parent&.rules || EMPTY_HASH
430
+ parents.reduce({}) { |rules, parent| rules.merge(parent.rules) }
384
431
  end
385
432
 
386
433
  # @api private
387
434
  def parent_key_map
388
- parent&.key_map || EMPTY_ARRAY
435
+ parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
389
436
  end
390
437
  end
391
438
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/logic/operators'
4
+ require 'dry/types/predicate_inferrer'
5
+ require 'dry/types/primitive_inferrer'
4
6
 
5
7
  require 'dry/schema/macros/core'
6
8
 
@@ -26,7 +28,13 @@ module Dry
26
28
  # PredicateInferrer is used to infer predicate type-check from a type spec
27
29
  # @return [PredicateInferrer]
28
30
  # @api private
29
- option :predicate_inferrer, default: proc { PredicateInferrer.new(compiler.predicates) }
31
+ option :predicate_inferrer, default: proc { ::Dry::Types::PredicateInferrer.new(compiler.predicates) }
32
+
33
+ # @!attribute [r] primitive_inferrer
34
+ # PrimitiveInferrer used to get a list of primitive classes from configured type
35
+ # @return [PrimitiveInferrer]
36
+ # @api private
37
+ option :primitive_inferrer, default: proc { ::Dry::Types::PrimitiveInferrer.new }
30
38
 
31
39
  # @overload value(*predicates, **predicate_opts)
32
40
  # Set predicates without and with arguments
@@ -192,9 +200,7 @@ module Dry
192
200
  predicates = Array(type_spec ? args[1..-1] : args)
193
201
 
194
202
  if type_spec
195
- resolved_type = schema_dsl.resolve_type(
196
- nullable && !type_spec.is_a?(::Array) ? [:nil, type_spec] : type_spec
197
- )
203
+ resolved_type = resolve_type(type_spec, nullable)
198
204
 
199
205
  type(resolved_type) if set_type
200
206
 
@@ -207,6 +213,17 @@ module Dry
207
213
 
208
214
  yield(*predicates, type_spec: type_spec)
209
215
  end
216
+
217
+ # @api private
218
+ def resolve_type(type_spec, nullable)
219
+ resolved = schema_dsl.resolve_type(type_spec)
220
+
221
+ if type_spec.is_a?(::Array) || !nullable || resolved.optional?
222
+ resolved
223
+ else
224
+ schema_dsl.resolve_type([:nil, resolved])
225
+ end
226
+ end
210
227
  end
211
228
  end
212
229
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/primitive_inferrer'
4
3
  require 'dry/schema/macros/value'
5
4
 
6
5
  module Dry
@@ -10,12 +9,6 @@ module Dry
10
9
  #
11
10
  # @api private
12
11
  class Filled < Value
13
- # @!attribute [r] primitive_inferrer
14
- # PrimitiveInferrer used to get a list of primitive classes from configured type
15
- # @return [PrimitiveInferrer]
16
- # @api private
17
- option :primitive_inferrer, default: proc { PrimitiveInferrer.new }
18
-
19
12
  # @api private
20
13
  def call(*predicates, **opts, &block)
21
14
  ensure_valid_predicates(predicates)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/predicate_inferrer'
4
3
  require 'dry/schema/processor'
5
4
  require 'dry/schema/macros/dsl'
6
5
  require 'dry/schema/constants'
@@ -28,8 +28,8 @@ module Dry
28
28
  definition = schema_dsl.new(&block)
29
29
  schema = definition.call
30
30
  type_schema =
31
- if array?
32
- parent_type.of(definition.type_schema)
31
+ if array_type?(parent_type)
32
+ build_array_type(parent_type, definition.type_schema)
33
33
  elsif redefined_schema?(args)
34
34
  parent_type.schema(definition.types)
35
35
  else
@@ -56,11 +56,6 @@ module Dry
56
56
  parent_type.optional?
57
57
  end
58
58
 
59
- # @api private
60
- def array?
61
- parent_type.respond_to?(:of)
62
- end
63
-
64
59
  # @api private
65
60
  def schema?
66
61
  parent_type.respond_to?(:schema)
@@ -17,8 +17,8 @@ module Dry
17
17
  current_type = schema_dsl.types[name]
18
18
 
19
19
  updated_type =
20
- if current_type.respond_to?(:of)
21
- current_type.of(schema.type_schema)
20
+ if array_type?(current_type)
21
+ build_array_type(current_type, schema.type_schema)
22
22
  else
23
23
  schema.type_schema
24
24
  end
@@ -39,6 +39,25 @@ module Dry
39
39
  self
40
40
  end
41
41
 
42
+ # @api private
43
+ def array_type?(type)
44
+ primitive_inferrer[type].eql?([::Array])
45
+ end
46
+
47
+ # @api private
48
+ def build_array_type(array_type, member)
49
+ if array_type.respond_to?(:of)
50
+ array_type.of(member)
51
+ else
52
+ raise ArgumentError, <<~ERROR.split("\n").join(' ')
53
+ Cannot define schema for a nominal array type.
54
+ Array types must be instances of Dry::Types::Array,
55
+ usually constructed with Types::Constructor(Array) { ... } or
56
+ Dry::Types['array'].constructor { ... }
57
+ ERROR
58
+ end
59
+ end
60
+
42
61
  # @api private
43
62
  def respond_to_missing?(meth, include_private = false)
44
63
  super || meth.to_s.end_with?(QUESTION_MARK)
@@ -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,30 +92,34 @@ 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
 
@@ -130,7 +134,13 @@ module Dry
130
134
  # rubocop:enable Metrics/AbcSize
131
135
 
132
136
  # @api private
133
- def lookup_paths(tokens)
137
+ def lookup_paths(predicate, options)
138
+ tokens = lookup_tokens(predicate, options)
139
+ filled_lookup_paths(tokens)
140
+ end
141
+
142
+ # @api private
143
+ def filled_lookup_paths(tokens)
134
144
  config.lookup_paths.map { |path| path % tokens }
135
145
  end
136
146
 
@@ -167,8 +177,28 @@ module Dry
167
177
  config.default_locale
168
178
  end
169
179
 
180
+ # @api private
181
+ def cache_key(predicate, options)
182
+ if options.key?(:input)
183
+ [predicate, options.reject { |k,| k.equal?(:input) }]
184
+ else
185
+ [predicate, options]
186
+ end
187
+ end
188
+
170
189
  private
171
190
 
191
+ # @api private
192
+ def lookup_tokens(predicate, options)
193
+ options.merge(
194
+ predicate: predicate,
195
+ root: options[:not] ? "#{root}.not" : root,
196
+ arg_type: config.arg_types[options[:arg_type]],
197
+ val_type: config.val_types[options[:val_type]],
198
+ message_type: options[:message_type] || :failure
199
+ )
200
+ end
201
+
172
202
  # @api private
173
203
  def custom_top_namespace?(path)
174
204
  path.to_s == DEFAULT_MESSAGES_PATH.to_s && config.top_namespace != DEFAULT_MESSAGES_ROOT
@@ -79,6 +79,15 @@ module Dry
79
79
  self
80
80
  end
81
81
 
82
+ # @api private
83
+ def cache_key(predicate, options)
84
+ if options[:locale]
85
+ super
86
+ else
87
+ [*super, I18n.locale]
88
+ end
89
+ end
90
+
82
91
  private
83
92
 
84
93
  # @api private
@@ -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
@@ -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