dry-schema 1.3.1 → 1.4.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +174 -107
- data/README.md +3 -1
- data/config/errors.yml +9 -0
- data/lib/dry/schema/constants.rb +3 -2
- data/lib/dry/schema/dsl.rb +63 -16
- data/lib/dry/schema/macros/dsl.rb +21 -4
- data/lib/dry/schema/macros/filled.rb +0 -7
- data/lib/dry/schema/macros/key.rb +0 -1
- data/lib/dry/schema/macros/schema.rb +2 -7
- data/lib/dry/schema/macros/value.rb +21 -2
- data/lib/dry/schema/message_compiler.rb +2 -1
- data/lib/dry/schema/messages/abstract.rb +42 -12
- data/lib/dry/schema/messages/i18n.rb +9 -0
- data/lib/dry/schema/messages/namespaced.rb +6 -1
- data/lib/dry/schema/messages/yaml.rb +12 -0
- data/lib/dry/schema/namespaced_rule.rb +2 -1
- data/lib/dry/schema/predicate_registry.rb +2 -23
- data/lib/dry/schema/processor.rb +9 -26
- data/lib/dry/schema/processor_steps.rb +116 -0
- data/lib/dry/schema/type_registry.rb +2 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +5 -5
- data/lib/dry/schema/primitive_inferrer.rb +0 -98
data/lib/dry/schema/constants.rb
CHANGED
@@ -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
|
data/lib/dry/schema/dsl.rb
CHANGED
@@ -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 [
|
68
|
-
option :parent,
|
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
|
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 =
|
193
|
-
steps
|
194
|
-
steps
|
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
|
-
|
225
|
-
|
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:
|
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
|
-
|
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
|
327
|
-
|
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
|
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
|
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 =
|
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)
|
@@ -28,8 +28,8 @@ module Dry
|
|
28
28
|
definition = schema_dsl.new(&block)
|
29
29
|
schema = definition.call
|
30
30
|
type_schema =
|
31
|
-
if
|
32
|
-
parent_type
|
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
|
21
|
-
current_type
|
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] ||
|
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(
|
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(
|
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
|
@@ -55,7 +55,7 @@ module Dry
|
|
55
55
|
end
|
56
56
|
|
57
57
|
# @api private
|
58
|
-
def
|
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
|