dry-schema 1.2.0 → 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 +214 -100
- data/README.md +4 -2
- data/config/errors.yml +9 -0
- data/lib/dry/schema/constants.rb +3 -2
- data/lib/dry/schema/dsl.rb +64 -17
- data/lib/dry/schema/extensions/hints/message_set_methods.rb +48 -1
- data/lib/dry/schema/macros/dsl.rb +22 -11
- 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 +18 -7
- data/lib/dry/schema/macros/value.rb +21 -2
- data/lib/dry/schema/message.rb +1 -1
- 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/path.rb +10 -6
- data/lib/dry/schema/predicate_inferrer.rb +69 -16
- 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/trace.rb +15 -20
- 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/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
[gem]: https://rubygems.org/gems/dry-schema
|
2
|
-
[travis]: https://travis-ci.
|
2
|
+
[travis]: https://travis-ci.com/dry-rb/dry-schema
|
3
|
+
[actions]: https://github.com/dry-rb/dry-schema/actions
|
3
4
|
[codeclimate]: https://codeclimate.com/github/dry-rb/dry-schema
|
4
5
|
[chat]: https://dry-rb.zulipchat.com
|
5
6
|
[inchpages]: http://inch-ci.org/github/dry-rb/dry-schema
|
@@ -7,7 +8,8 @@
|
|
7
8
|
# dry-schema [][chat]
|
8
9
|
|
9
10
|
[][gem]
|
10
|
-
[][travis]
|
12
|
+
[][actions]
|
11
13
|
[][codeclimate]
|
12
14
|
[][codeclimate]
|
13
15
|
[][inchpages]
|
data/config/errors.yml
CHANGED
@@ -73,8 +73,12 @@ en:
|
|
73
73
|
|
74
74
|
max_size?: "size cannot be greater than %{num}"
|
75
75
|
|
76
|
+
max_bytesize?: "bytesize cannot be greater than %{num}"
|
77
|
+
|
76
78
|
min_size?: "size cannot be less than %{num}"
|
77
79
|
|
80
|
+
min_bytesize?: "bytesize cannot be less than %{num}"
|
81
|
+
|
78
82
|
nil?: "cannot be defined"
|
79
83
|
|
80
84
|
str?: "must be a string"
|
@@ -92,5 +96,10 @@ en:
|
|
92
96
|
default: "length must be %{size}"
|
93
97
|
range: "length must be within %{size_left} - %{size_right}"
|
94
98
|
|
99
|
+
bytesize?:
|
100
|
+
arg:
|
101
|
+
default: "must be %{size} bytes long"
|
102
|
+
range: "must be within %{size_left} - %{size_right} bytes long"
|
103
|
+
|
95
104
|
not:
|
96
105
|
empty?: "cannot be empty"
|
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
|
@@ -231,7 +276,7 @@ module Dry
|
|
231
276
|
#
|
232
277
|
# @api private
|
233
278
|
def new(options = EMPTY_HASH, &block)
|
234
|
-
self.class.new(options.merge(processor_type: processor_type), &block)
|
279
|
+
self.class.new(options.merge(processor_type: processor_type, config: config), &block)
|
235
280
|
end
|
236
281
|
|
237
282
|
# Set a type for the given key name
|
@@ -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
|
@@ -12,7 +12,7 @@ module Dry
|
|
12
12
|
#
|
13
13
|
# @return [Array<Message::Hint>]
|
14
14
|
attr_reader :hints
|
15
|
-
|
15
|
+
|
16
16
|
# Configuration option to enable/disable showing errors
|
17
17
|
#
|
18
18
|
# @return [Boolean]
|
@@ -37,6 +37,53 @@ module Dry
|
|
37
37
|
@to_h ||= failures ? messages_map : messages_map(hints)
|
38
38
|
end
|
39
39
|
alias_method :to_hash, :to_h
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def unique_paths
|
45
|
+
messages.uniq(&:path).map(&:path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @api private
|
49
|
+
def messages_map(messages = self.messages)
|
50
|
+
return EMPTY_HASH if empty?
|
51
|
+
|
52
|
+
messages.reduce(placeholders) { |hash, msg|
|
53
|
+
node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
|
54
|
+
(node[0].is_a?(::Array) ? node[0] : node) << msg.dump
|
55
|
+
hash
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# @api private
|
60
|
+
#
|
61
|
+
# rubocop:disable Metrics/AbcSize
|
62
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
63
|
+
def initialize_placeholders!
|
64
|
+
@placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
|
65
|
+
curr_idx = 0
|
66
|
+
last_idx = path.size - 1
|
67
|
+
node = hash
|
68
|
+
|
69
|
+
while curr_idx <= last_idx
|
70
|
+
key = path[curr_idx]
|
71
|
+
|
72
|
+
next_node =
|
73
|
+
if node.is_a?(Array) && key.is_a?(Symbol)
|
74
|
+
node_hash = (node << [] << {}).last
|
75
|
+
node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
|
76
|
+
else
|
77
|
+
node[key] || (node[key] = curr_idx < last_idx ? {} : [])
|
78
|
+
end
|
79
|
+
|
80
|
+
node = next_node
|
81
|
+
curr_idx += 1
|
82
|
+
end
|
83
|
+
}
|
84
|
+
end
|
85
|
+
# rubocop:enable Metrics/AbcSize
|
86
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
40
87
|
end
|
41
88
|
end
|
42
89
|
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,27 +200,30 @@ 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
|
|
201
207
|
type_predicates = predicate_inferrer[resolved_type]
|
202
208
|
|
203
|
-
unless type_predicates.empty?
|
204
|
-
if type_predicates.is_a?(::Array) && type_predicates.size.equal?(1)
|
205
|
-
predicates.unshift(type_predicates[0])
|
206
|
-
else
|
207
|
-
predicates.unshift(type_predicates)
|
208
|
-
end
|
209
|
-
end
|
209
|
+
predicates.replace(type_predicates + predicates) unless type_predicates.empty?
|
210
210
|
|
211
211
|
return self if predicates.empty?
|
212
212
|
end
|
213
213
|
|
214
214
|
yield(*predicates, type_spec: type_spec)
|
215
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
|
216
227
|
end
|
217
228
|
end
|
218
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)
|
@@ -11,10 +11,10 @@ module Dry
|
|
11
11
|
class Schema < Value
|
12
12
|
# @api private
|
13
13
|
def call(*args, &block)
|
14
|
-
super(*args) unless args.empty?
|
14
|
+
super(*args, &nil) unless args.empty?
|
15
15
|
|
16
16
|
if block
|
17
|
-
schema = define(&block)
|
17
|
+
schema = define(*args, &block)
|
18
18
|
trace << schema.to_rule
|
19
19
|
end
|
20
20
|
|
@@ -24,11 +24,17 @@ module Dry
|
|
24
24
|
private
|
25
25
|
|
26
26
|
# @api private
|
27
|
-
def define(&block)
|
27
|
+
def define(*args, &block)
|
28
28
|
definition = schema_dsl.new(&block)
|
29
29
|
schema = definition.call
|
30
|
-
|
31
|
-
|
30
|
+
type_schema =
|
31
|
+
if array_type?(parent_type)
|
32
|
+
build_array_type(parent_type, definition.type_schema)
|
33
|
+
elsif redefined_schema?(args)
|
34
|
+
parent_type.schema(definition.types)
|
35
|
+
else
|
36
|
+
definition.type_schema
|
37
|
+
end
|
32
38
|
final_type = optional? ? type_schema.optional : type_schema
|
33
39
|
|
34
40
|
type(final_type)
|
@@ -51,8 +57,13 @@ module Dry
|
|
51
57
|
end
|
52
58
|
|
53
59
|
# @api private
|
54
|
-
def
|
55
|
-
parent_type.respond_to?(:
|
60
|
+
def schema?
|
61
|
+
parent_type.respond_to?(:schema)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def redefined_schema?(args)
|
66
|
+
schema? && args.first.is_a?(Processor)
|
56
67
|
end
|
57
68
|
end
|
58
69
|
end
|
@@ -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)
|