dry-schema 1.4.3 → 1.5.0
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 +170 -97
- data/config/errors.yml +4 -0
- data/dry-schema.gemspec +46 -0
- data/lib/dry-schema.rb +1 -1
- data/lib/dry/schema.rb +19 -6
- data/lib/dry/schema/compiler.rb +4 -4
- data/lib/dry/schema/config.rb +15 -6
- data/lib/dry/schema/constants.rb +16 -7
- data/lib/dry/schema/dsl.rb +88 -27
- data/lib/dry/schema/extensions.rb +10 -2
- data/lib/dry/schema/extensions/hints.rb +15 -8
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
- data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
- data/lib/dry/schema/extensions/info.rb +27 -0
- data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
- data/lib/dry/schema/extensions/monads.rb +1 -1
- data/lib/dry/schema/extensions/struct.rb +32 -0
- data/lib/dry/schema/json.rb +1 -1
- data/lib/dry/schema/key.rb +16 -1
- data/lib/dry/schema/key_coercer.rb +4 -4
- data/lib/dry/schema/key_map.rb +9 -4
- data/lib/dry/schema/key_validator.rb +66 -0
- data/lib/dry/schema/macros.rb +8 -8
- data/lib/dry/schema/macros/array.rb +17 -4
- data/lib/dry/schema/macros/core.rb +9 -4
- data/lib/dry/schema/macros/dsl.rb +34 -19
- data/lib/dry/schema/macros/each.rb +4 -4
- data/lib/dry/schema/macros/filled.rb +5 -5
- data/lib/dry/schema/macros/hash.rb +21 -3
- data/lib/dry/schema/macros/key.rb +9 -9
- data/lib/dry/schema/macros/maybe.rb +3 -3
- data/lib/dry/schema/macros/optional.rb +1 -1
- data/lib/dry/schema/macros/required.rb +1 -1
- data/lib/dry/schema/macros/schema.rb +23 -2
- data/lib/dry/schema/macros/value.rb +32 -10
- data/lib/dry/schema/message.rb +35 -9
- data/lib/dry/schema/message/or.rb +18 -39
- data/lib/dry/schema/message/or/abstract.rb +28 -0
- data/lib/dry/schema/message/or/multi_path.rb +37 -0
- data/lib/dry/schema/message/or/single_path.rb +64 -0
- data/lib/dry/schema/message_compiler.rb +37 -17
- data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
- data/lib/dry/schema/message_set.rb +25 -36
- data/lib/dry/schema/messages.rb +6 -6
- data/lib/dry/schema/messages/abstract.rb +54 -56
- data/lib/dry/schema/messages/i18n.rb +29 -27
- data/lib/dry/schema/messages/namespaced.rb +12 -2
- data/lib/dry/schema/messages/template.rb +19 -44
- data/lib/dry/schema/messages/yaml.rb +60 -13
- data/lib/dry/schema/params.rb +1 -1
- data/lib/dry/schema/path.rb +44 -5
- data/lib/dry/schema/predicate.rb +2 -2
- data/lib/dry/schema/predicate_inferrer.rb +4 -184
- data/lib/dry/schema/predicate_registry.rb +2 -2
- data/lib/dry/schema/primitive_inferrer.rb +16 -0
- data/lib/dry/schema/processor.rb +49 -28
- data/lib/dry/schema/processor_steps.rb +50 -27
- data/lib/dry/schema/result.rb +43 -5
- data/lib/dry/schema/rule_applier.rb +8 -7
- data/lib/dry/schema/step.rb +79 -0
- data/lib/dry/schema/trace.rb +5 -4
- data/lib/dry/schema/type_container.rb +3 -3
- data/lib/dry/schema/type_registry.rb +2 -2
- data/lib/dry/schema/types.rb +1 -1
- data/lib/dry/schema/value_coercer.rb +2 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +22 -8
@@ -37,53 +37,6 @@ 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
|
87
40
|
end
|
88
41
|
end
|
89
42
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/extensions/info/schema_compiler"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# Info extension
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
module Info
|
11
|
+
module SchemaMethods
|
12
|
+
# Return information about keys and types
|
13
|
+
#
|
14
|
+
# @return [Hash<Symbol=>Hash>]
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def info
|
18
|
+
compiler = SchemaCompiler.new
|
19
|
+
compiler.call(to_ast)
|
20
|
+
compiler.to_h
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Processor.include(Info::SchemaMethods)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/constants"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# @api private
|
8
|
+
module Info
|
9
|
+
# @api private
|
10
|
+
class SchemaCompiler
|
11
|
+
PREDICATE_TO_TYPE = {
|
12
|
+
array?: "array",
|
13
|
+
bool?: "bool",
|
14
|
+
date?: "date",
|
15
|
+
date_time?: "date_time",
|
16
|
+
decimal?: "float",
|
17
|
+
float?: "float",
|
18
|
+
hash?: "hash",
|
19
|
+
int?: "integer",
|
20
|
+
nil?: "nil",
|
21
|
+
str?: "string",
|
22
|
+
time?: "time"
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
attr_reader :keys
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def initialize
|
30
|
+
@keys = EMPTY_HASH.dup
|
31
|
+
end
|
32
|
+
|
33
|
+
# @api private
|
34
|
+
def to_h
|
35
|
+
{keys: keys}
|
36
|
+
end
|
37
|
+
|
38
|
+
# @api private
|
39
|
+
def call(ast)
|
40
|
+
visit(ast)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def visit(node, opts = EMPTY_HASH)
|
45
|
+
meth, rest = node
|
46
|
+
public_send(:"visit_#{meth}", rest, opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
def visit_set(node, opts = EMPTY_HASH)
|
51
|
+
target = (key = opts[:key]) ? self.class.new : self
|
52
|
+
|
53
|
+
node.map { |child| target.visit(child, opts) }
|
54
|
+
|
55
|
+
return unless key
|
56
|
+
|
57
|
+
target_info = opts[:member] ? {member: target.to_h} : target.to_h
|
58
|
+
type = opts[:member] ? "array" : "hash"
|
59
|
+
|
60
|
+
keys.update(key => {**keys[key], type: type, **target_info})
|
61
|
+
end
|
62
|
+
|
63
|
+
# @api private
|
64
|
+
def visit_and(node, opts = EMPTY_HASH)
|
65
|
+
left, right = node
|
66
|
+
|
67
|
+
visit(left, opts)
|
68
|
+
visit(right, opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @api private
|
72
|
+
def visit_implication(node, opts = EMPTY_HASH)
|
73
|
+
node.each do |el|
|
74
|
+
visit(el, opts.merge(required: false))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def visit_each(node, opts = EMPTY_HASH)
|
80
|
+
visit(node, opts.merge(member: true))
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def visit_key(node, opts = EMPTY_HASH)
|
85
|
+
name, rest = node
|
86
|
+
visit(rest, opts.merge(key: name, required: true))
|
87
|
+
end
|
88
|
+
|
89
|
+
# @api private
|
90
|
+
def visit_predicate(node, opts = EMPTY_HASH)
|
91
|
+
name, rest = node
|
92
|
+
|
93
|
+
key = opts[:key]
|
94
|
+
|
95
|
+
if name.equal?(:key?)
|
96
|
+
keys[rest[0][1]] = {required: opts.fetch(:required, true)}
|
97
|
+
else
|
98
|
+
type = PREDICATE_TO_TYPE[name]
|
99
|
+
keys[key][:type] = type if type
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/struct"
|
4
|
+
require "dry/schema/predicate_inferrer"
|
5
|
+
require "dry/schema/primitive_inferrer"
|
6
|
+
require "dry/schema/macros/dsl"
|
7
|
+
require "dry/schema/macros/hash"
|
8
|
+
|
9
|
+
module Dry
|
10
|
+
module Schema
|
11
|
+
module Macros
|
12
|
+
Hash.prepend(::Module.new {
|
13
|
+
def call(*args)
|
14
|
+
if args.size >= 1 && args[0].is_a?(::Class) && args[0] <= ::Dry::Struct
|
15
|
+
if block_given?
|
16
|
+
raise ArgumentError, "blocks are not supported when using "\
|
17
|
+
"a struct class (#{name.inspect} => #{args[0]})"
|
18
|
+
end
|
19
|
+
|
20
|
+
super(args[0].schema, *args.drop(1))
|
21
|
+
type(schema_dsl.types[name].constructor(args[0]))
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
})
|
27
|
+
end
|
28
|
+
|
29
|
+
PredicateInferrer::Compiler.send(:alias_method, :visit_struct, :visit_hash)
|
30
|
+
PrimitiveInferrer::Compiler.send(:alias_method, :visit_struct, :visit_hash)
|
31
|
+
end
|
32
|
+
end
|
data/lib/dry/schema/json.rb
CHANGED
data/lib/dry/schema/key.rb
CHANGED
@@ -64,6 +64,11 @@ module Dry
|
|
64
64
|
new(name: name.to_s)
|
65
65
|
end
|
66
66
|
|
67
|
+
# @api private
|
68
|
+
def to_dot_notation
|
69
|
+
[name.to_s]
|
70
|
+
end
|
71
|
+
|
67
72
|
# @api private
|
68
73
|
def new(**new_opts)
|
69
74
|
self.class.new(id, name: name, coercer: coercer, **new_opts)
|
@@ -118,9 +123,14 @@ module Dry
|
|
118
123
|
new(name: name.to_s, members: members.stringified)
|
119
124
|
end
|
120
125
|
|
126
|
+
# @api private
|
127
|
+
def to_dot_notation
|
128
|
+
[name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
|
129
|
+
end
|
130
|
+
|
121
131
|
# @api private
|
122
132
|
def dump
|
123
|
-
{
|
133
|
+
{name => members.map(&:dump)}
|
124
134
|
end
|
125
135
|
end
|
126
136
|
|
@@ -155,6 +165,11 @@ module Dry
|
|
155
165
|
new(name: name.to_s, member: member.stringified)
|
156
166
|
end
|
157
167
|
|
168
|
+
# @api private
|
169
|
+
def to_dot_notation
|
170
|
+
[:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
|
171
|
+
end
|
172
|
+
|
158
173
|
# @api private
|
159
174
|
def dump
|
160
175
|
[name, member.dump]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/core/cache"
|
4
|
+
require "dry/equalizer"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Schema
|
@@ -32,8 +32,8 @@ module Dry
|
|
32
32
|
end
|
33
33
|
|
34
34
|
# @api private
|
35
|
-
def call(
|
36
|
-
key_map.write(
|
35
|
+
def call(result)
|
36
|
+
key_map.write(result.to_h)
|
37
37
|
end
|
38
38
|
alias_method :[], :call
|
39
39
|
end
|
data/lib/dry/schema/key_map.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "dry/equalizer"
|
4
|
+
require "dry/core/cache"
|
5
|
+
require "dry/schema/constants"
|
6
|
+
require "dry/schema/key"
|
7
7
|
|
8
8
|
module Dry
|
9
9
|
module Schema
|
@@ -100,6 +100,11 @@ module Dry
|
|
100
100
|
self.class.new(map(&:stringified))
|
101
101
|
end
|
102
102
|
|
103
|
+
# @api private
|
104
|
+
def to_dot_notation
|
105
|
+
@to_dot_notation ||= map(&:to_dot_notation).flatten
|
106
|
+
end
|
107
|
+
|
103
108
|
# Iterate over keys
|
104
109
|
#
|
105
110
|
# @api public
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/schema/constants"
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Schema
|
8
|
+
# @api private
|
9
|
+
class KeyValidator
|
10
|
+
extend Dry::Initializer
|
11
|
+
|
12
|
+
INDEX_REGEX = /\[\d+\]/.freeze
|
13
|
+
DIGIT_REGEX = /\A\d+\z/.freeze
|
14
|
+
BRACKETS = "[]"
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
option :key_map
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def call(result)
|
21
|
+
input = result.to_h
|
22
|
+
|
23
|
+
input_paths = key_paths(input)
|
24
|
+
key_paths = key_map.to_dot_notation
|
25
|
+
|
26
|
+
input_paths.each do |path|
|
27
|
+
error_path =
|
28
|
+
if path[INDEX_REGEX]
|
29
|
+
key = path.gsub(INDEX_REGEX, BRACKETS)
|
30
|
+
|
31
|
+
unless key_paths.include?(key)
|
32
|
+
arr = path.gsub(INDEX_REGEX) { |m| ".#{m[1]}" }
|
33
|
+
arr.split(DOT).map { |s| DIGIT_REGEX.match?(s) ? s.to_i : s.to_sym }
|
34
|
+
end
|
35
|
+
elsif !key_paths.include?(path)
|
36
|
+
path
|
37
|
+
end
|
38
|
+
|
39
|
+
next unless error_path
|
40
|
+
|
41
|
+
result.add_error([:unexpected_key, [error_path, input]])
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @api private
|
50
|
+
def key_paths(hash)
|
51
|
+
hash.flat_map { |key, _|
|
52
|
+
case (value = hash[key])
|
53
|
+
when Hash
|
54
|
+
[key].product(key_paths(hash[key])).map { |keys| keys.join(DOT) }
|
55
|
+
when Array
|
56
|
+
value.flat_map.with_index { |el, idx|
|
57
|
+
key_paths(el).map { |path| ["#{key}[#{idx}]", *path].join(DOT) }
|
58
|
+
}
|
59
|
+
else
|
60
|
+
key.to_s
|
61
|
+
end
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/dry/schema/macros.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
3
|
+
require "dry/schema/macros/array"
|
4
|
+
require "dry/schema/macros/each"
|
5
|
+
require "dry/schema/macros/filled"
|
6
|
+
require "dry/schema/macros/schema"
|
7
|
+
require "dry/schema/macros/hash"
|
8
|
+
require "dry/schema/macros/maybe"
|
9
|
+
require "dry/schema/macros/optional"
|
10
|
+
require "dry/schema/macros/required"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/schema/macros/dsl"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Schema
|
@@ -13,16 +13,29 @@ module Dry
|
|
13
13
|
def value(*args, **opts, &block)
|
14
14
|
type(:array)
|
15
15
|
|
16
|
-
extract_type_spec(*args, set_type: false) do |*predicates, type_spec:|
|
16
|
+
extract_type_spec(*args, set_type: false) do |*predicates, type_spec:, type_rule:|
|
17
17
|
type(schema_dsl.array[type_spec]) if type_spec
|
18
18
|
|
19
19
|
is_hash_block = type_spec.equal?(:hash)
|
20
20
|
|
21
21
|
if predicates.any? || opts.any? || !is_hash_block
|
22
|
-
super(
|
22
|
+
super(
|
23
|
+
*predicates, type_spec: type_spec, type_rule: type_rule, **opts,
|
24
|
+
&(is_hash_block ? nil : block)
|
25
|
+
)
|
23
26
|
end
|
24
27
|
|
25
|
-
|
28
|
+
is_op = args.size.equal?(2) && args[1].is_a?(Logic::Operations::Abstract)
|
29
|
+
|
30
|
+
if is_hash_block && !is_op
|
31
|
+
hash(&block)
|
32
|
+
elsif is_op
|
33
|
+
hash = Value.new(schema_dsl: schema_dsl.new, name: name).hash(args[1])
|
34
|
+
|
35
|
+
trace.captures.concat(hash.trace.captures)
|
36
|
+
|
37
|
+
type(schema_dsl.types[name].of(hash.schema_dsl.types[name]))
|
38
|
+
end
|
26
39
|
end
|
27
40
|
|
28
41
|
self
|