dry-schema 1.3.4 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +253 -101
- data/LICENSE +1 -1
- data/README.md +6 -6
- data/config/errors.yml +4 -0
- data/dry-schema.gemspec +46 -0
- data/lib/dry-schema.rb +1 -1
- data/lib/dry/schema.rb +20 -7
- data/lib/dry/schema/compiler.rb +4 -4
- data/lib/dry/schema/config.rb +15 -6
- data/lib/dry/schema/constants.rb +19 -9
- data/lib/dry/schema/dsl.rb +144 -38
- 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 +2 -2
- 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 +20 -5
- 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 +11 -6
- data/lib/dry/schema/macros/dsl.rb +53 -21
- data/lib/dry/schema/macros/each.rb +4 -4
- data/lib/dry/schema/macros/filled.rb +5 -6
- data/lib/dry/schema/macros/hash.rb +21 -3
- data/lib/dry/schema/macros/key.rb +10 -10
- data/lib/dry/schema/macros/maybe.rb +4 -5
- 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 +34 -7
- 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 +40 -19
- data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
- data/lib/dry/schema/message_set.rb +26 -37
- data/lib/dry/schema/messages.rb +6 -6
- data/lib/dry/schema/messages/abstract.rb +79 -66
- data/lib/dry/schema/messages/i18n.rb +36 -10
- data/lib/dry/schema/messages/namespaced.rb +13 -3
- data/lib/dry/schema/messages/template.rb +19 -44
- data/lib/dry/schema/messages/yaml.rb +72 -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 +3 -24
- data/lib/dry/schema/primitive_inferrer.rb +3 -86
- data/lib/dry/schema/processor.rb +54 -50
- data/lib/dry/schema/processor_steps.rb +139 -0
- data/lib/dry/schema/result.rb +52 -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 +21 -7
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/schema/path"
|
4
|
+
require "dry/schema/macros/dsl"
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Schema
|
@@ -13,6 +14,8 @@ module Dry
|
|
13
14
|
def call(*predicates, **opts, &block)
|
14
15
|
schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
|
15
16
|
|
17
|
+
type_spec = opts[:type_spec]
|
18
|
+
|
16
19
|
if schema
|
17
20
|
current_type = schema_dsl.types[name]
|
18
21
|
|
@@ -23,17 +26,32 @@ module Dry
|
|
23
26
|
schema.type_schema
|
24
27
|
end
|
25
28
|
|
26
|
-
|
29
|
+
import_steps(schema)
|
30
|
+
|
31
|
+
type(updated_type) unless custom_type? && !current_type.respond_to?(:of)
|
27
32
|
end
|
28
33
|
|
29
|
-
|
30
|
-
|
34
|
+
trace_opts = opts.reject { |key, _| key == :type_spec || key == :type_rule }
|
35
|
+
|
36
|
+
if (type_rule = opts[:type_rule])
|
37
|
+
trace.append(type_rule).evaluate(*predicates, **trace_opts)
|
38
|
+
trace.append(new(chain: false).instance_exec(&block)) if block
|
39
|
+
else
|
40
|
+
trace.evaluate(*predicates, **trace_opts)
|
41
|
+
|
42
|
+
if block && type_spec.equal?(:hash)
|
43
|
+
hash(&block)
|
44
|
+
elsif type_spec.is_a?(::Dry::Types::Type) && hash_type?(type_spec)
|
45
|
+
hash(type_spec)
|
46
|
+
elsif block
|
47
|
+
trace.append(new(chain: false).instance_exec(&block))
|
48
|
+
end
|
49
|
+
end
|
31
50
|
|
32
51
|
if trace.captures.empty?
|
33
|
-
raise ArgumentError,
|
52
|
+
raise ArgumentError, "wrong number of arguments (given 0, expected at least 1)"
|
34
53
|
end
|
35
54
|
|
36
|
-
type_spec = opts[:type_spec]
|
37
55
|
each(type_spec.type.member) if type_spec.respond_to?(:member)
|
38
56
|
|
39
57
|
self
|
@@ -44,12 +62,16 @@ module Dry
|
|
44
62
|
primitive_inferrer[type].eql?([::Array])
|
45
63
|
end
|
46
64
|
|
65
|
+
def hash_type?(type)
|
66
|
+
primitive_inferrer[type].eql?([::Hash])
|
67
|
+
end
|
68
|
+
|
47
69
|
# @api private
|
48
70
|
def build_array_type(array_type, member)
|
49
71
|
if array_type.respond_to?(:of)
|
50
72
|
array_type.of(member)
|
51
73
|
else
|
52
|
-
raise ArgumentError, <<~ERROR.split("\n").join(
|
74
|
+
raise ArgumentError, <<~ERROR.split("\n").join(" ")
|
53
75
|
Cannot define schema for a nominal array type.
|
54
76
|
Array types must be instances of Dry::Types::Array,
|
55
77
|
usually constructed with Types::Constructor(Array) { ... } or
|
@@ -58,6 +80,11 @@ module Dry
|
|
58
80
|
end
|
59
81
|
end
|
60
82
|
|
83
|
+
# @api private
|
84
|
+
def import_steps(schema)
|
85
|
+
schema_dsl.steps.import_callbacks(Path[[*path, name]], schema.steps)
|
86
|
+
end
|
87
|
+
|
61
88
|
# @api private
|
62
89
|
def respond_to_missing?(meth, include_private = false)
|
63
90
|
super || meth.to_s.end_with?(QUESTION_MARK)
|
data/lib/dry/schema/message.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/equalizer"
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
6
|
+
require "dry/schema/path"
|
7
|
+
require "dry/schema/message/or"
|
8
8
|
|
9
9
|
module Dry
|
10
10
|
module Schema
|
@@ -52,9 +52,22 @@ module Dry
|
|
52
52
|
#
|
53
53
|
# @api public
|
54
54
|
def dump
|
55
|
-
@dump ||= meta.empty? ? text : {
|
55
|
+
@dump ||= meta.empty? ? text : {text: text, **meta}
|
56
|
+
end
|
57
|
+
alias_method :to_s, :dump
|
58
|
+
|
59
|
+
# Dump the message into a hash
|
60
|
+
#
|
61
|
+
# The hash will be deeply nested if the path's size is greater than 1
|
62
|
+
#
|
63
|
+
# @see Message#to_h
|
64
|
+
#
|
65
|
+
# @return [Hash]
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def to_h
|
69
|
+
@to_h ||= _path.to_h(dump)
|
56
70
|
end
|
57
|
-
alias to_s dump
|
58
71
|
|
59
72
|
# See if another message is the same
|
60
73
|
#
|
@@ -69,19 +82,32 @@ module Dry
|
|
69
82
|
other.is_a?(String) ? text == other : super
|
70
83
|
end
|
71
84
|
|
85
|
+
# @api private
|
86
|
+
def to_or(root)
|
87
|
+
clone = dup
|
88
|
+
clone.instance_variable_set("@path", path - root.to_a)
|
89
|
+
clone.instance_variable_set("@_path", nil)
|
90
|
+
clone
|
91
|
+
end
|
92
|
+
|
72
93
|
# See which message is higher in the hierarchy
|
73
94
|
#
|
74
95
|
# @api private
|
75
96
|
def <=>(other)
|
76
|
-
l_path =
|
77
|
-
r_path =
|
97
|
+
l_path = _path
|
98
|
+
r_path = other._path
|
78
99
|
|
79
100
|
unless l_path.same_root?(r_path)
|
80
|
-
raise ArgumentError,
|
101
|
+
raise ArgumentError, "Cannot compare messages from different root paths"
|
81
102
|
end
|
82
103
|
|
83
104
|
l_path <=> r_path
|
84
105
|
end
|
106
|
+
|
107
|
+
# @api private
|
108
|
+
def _path
|
109
|
+
@_path ||= Path[path]
|
110
|
+
end
|
85
111
|
end
|
86
112
|
end
|
87
113
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/schema/message/or/single_path"
|
4
|
+
require "dry/schema/message/or/multi_path"
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Schema
|
@@ -8,45 +9,23 @@ module Dry
|
|
8
9
|
#
|
9
10
|
# @api public
|
10
11
|
class Message
|
11
|
-
|
12
|
-
#
|
13
|
-
# @api public
|
14
|
-
class Or
|
12
|
+
module Or
|
15
13
|
# @api private
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
@messages = messages
|
32
|
-
@path = left.path
|
33
|
-
end
|
34
|
-
|
35
|
-
# Dump a message into a string
|
36
|
-
#
|
37
|
-
# @see Message#dump
|
38
|
-
#
|
39
|
-
# @return [String]
|
40
|
-
#
|
41
|
-
# @api public
|
42
|
-
def dump
|
43
|
-
to_a.map(&:dump).join(" #{messages[:or][:text]} ")
|
44
|
-
end
|
45
|
-
alias to_s dump
|
46
|
-
|
47
|
-
# @api private
|
48
|
-
def to_a
|
49
|
-
[left, right]
|
14
|
+
def self.[](left, right, messages)
|
15
|
+
msgs = [left, right].flatten
|
16
|
+
paths = msgs.map(&:path)
|
17
|
+
|
18
|
+
if paths.uniq.size == 1
|
19
|
+
SinglePath.new(left, right, messages)
|
20
|
+
elsif right.is_a?(Array)
|
21
|
+
if left.is_a?(Array) && paths.uniq.size > 1
|
22
|
+
MultiPath.new(left, right)
|
23
|
+
else
|
24
|
+
right
|
25
|
+
end
|
26
|
+
else
|
27
|
+
msgs.max
|
28
|
+
end
|
50
29
|
end
|
51
30
|
end
|
52
31
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Schema
|
5
|
+
class Message
|
6
|
+
module Or
|
7
|
+
# A message type used by OR operations
|
8
|
+
#
|
9
|
+
# @abstract
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class Abstract
|
13
|
+
# @api private
|
14
|
+
attr_reader :left
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
attr_reader :right
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def initialize(left, right)
|
21
|
+
@left = left
|
22
|
+
@right = right
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/equalizer"
|
4
|
+
|
5
|
+
require "dry/schema/message/or/abstract"
|
6
|
+
require "dry/schema/path"
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Schema
|
10
|
+
class Message
|
11
|
+
module Or
|
12
|
+
# A message type used by OR operations with different paths
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
class MultiPath < Abstract
|
16
|
+
# @api private
|
17
|
+
attr_reader :root
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def initialize(*args)
|
21
|
+
super
|
22
|
+
@root = [left, right].flatten.map(&:_path).reduce(:&)
|
23
|
+
@left = left.map { |msg| msg.to_or(root) }
|
24
|
+
@right = right.map { |msg| msg.to_or(root) }
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api public
|
28
|
+
def to_h
|
29
|
+
@to_h ||= Path[[*root, :or]].to_h(
|
30
|
+
[left.map(&:to_h).reduce(:merge), right.map(&:to_h).reduce(:merge)]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/message/or/abstract"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
class Message
|
8
|
+
module Or
|
9
|
+
# A message type used by OR operations with the same path
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
class SinglePath < Abstract
|
13
|
+
# @api private
|
14
|
+
attr_reader :path
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
attr_reader :_path
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
attr_reader :messages
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def initialize(*args, messages)
|
24
|
+
super(*args)
|
25
|
+
@messages = messages
|
26
|
+
@path = left.path
|
27
|
+
@_path = left._path
|
28
|
+
end
|
29
|
+
|
30
|
+
# Dump a message into a string
|
31
|
+
#
|
32
|
+
# Both sides of the message will be joined using translated
|
33
|
+
# value under `dry_schema.or` message key
|
34
|
+
#
|
35
|
+
# @see Message#dump
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def dump
|
41
|
+
@dump ||= "#{left.dump} #{messages[:or][:text]} #{right.dump}"
|
42
|
+
end
|
43
|
+
alias_method :to_s, :dump
|
44
|
+
|
45
|
+
# Dump an `or` message into a hash
|
46
|
+
#
|
47
|
+
# @see Message#to_h
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def to_h
|
53
|
+
@to_h ||= _path.to_h(dump)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def to_a
|
58
|
+
@to_a ||= [left, right]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/initializer"
|
4
4
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
5
|
+
require "dry/schema/constants"
|
6
|
+
require "dry/schema/message"
|
7
|
+
require "dry/schema/message_set"
|
8
|
+
require "dry/schema/message_compiler/visitor_opts"
|
9
9
|
|
10
10
|
module Dry
|
11
11
|
module Schema
|
@@ -41,10 +41,10 @@ module Dry
|
|
41
41
|
attr_reader :default_lookup_options
|
42
42
|
|
43
43
|
# @api private
|
44
|
-
def initialize(messages, options
|
44
|
+
def initialize(messages, **options)
|
45
45
|
super
|
46
46
|
@options = options
|
47
|
-
@default_lookup_options = options[:locale] ? {
|
47
|
+
@default_lookup_options = options[:locale] ? {locale: locale} : EMPTY_HASH
|
48
48
|
end
|
49
49
|
|
50
50
|
# @api private
|
@@ -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
|
@@ -100,23 +100,35 @@ module Dry
|
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
|
+
# @api private
|
104
|
+
def visit_unexpected_key(node, _opts)
|
105
|
+
path, input = node
|
106
|
+
|
107
|
+
msg = messages.translate("errors.unexpected_key")
|
108
|
+
|
109
|
+
Message.new(
|
110
|
+
path: path,
|
111
|
+
text: msg[:text],
|
112
|
+
predicate: nil,
|
113
|
+
input: input
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
103
117
|
# @api private
|
104
118
|
def visit_or(node, opts)
|
105
119
|
left, right = node.map { |n| visit(n, opts) }
|
120
|
+
Message::Or[left, right, or_translator]
|
121
|
+
end
|
106
122
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
right
|
111
|
-
else
|
112
|
-
[left, right].flatten.max
|
113
|
-
end
|
123
|
+
# @api private
|
124
|
+
def or_translator
|
125
|
+
@or_translator ||= proc { |k| messages.translate(k, **default_lookup_options) }
|
114
126
|
end
|
115
127
|
|
116
128
|
# @api private
|
117
129
|
def visit_namespace(node, opts)
|
118
130
|
ns, rest = node
|
119
|
-
self.class.new(messages.namespaced(ns), options).visit(rest, opts)
|
131
|
+
self.class.new(messages.namespaced(ns), **options).visit(rest, opts)
|
120
132
|
end
|
121
133
|
|
122
134
|
# @api private
|
@@ -130,12 +142,21 @@ module Dry
|
|
130
142
|
path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
|
131
143
|
).to_h
|
132
144
|
|
133
|
-
template, meta = messages[predicate, options]
|
145
|
+
template, meta = messages[predicate, options]
|
146
|
+
|
147
|
+
unless template
|
148
|
+
raise MissingMessageError.new(path, messages.looked_up_paths(predicate, options))
|
149
|
+
end
|
134
150
|
|
135
151
|
text = message_text(template, tokens, options)
|
136
152
|
|
137
153
|
message_type(options).new(
|
138
|
-
text: text,
|
154
|
+
text: text,
|
155
|
+
meta: meta,
|
156
|
+
path: path,
|
157
|
+
predicate: predicate,
|
158
|
+
args: arg_vals,
|
159
|
+
input: input
|
139
160
|
)
|
140
161
|
end
|
141
162
|
|
@@ -179,7 +200,7 @@ module Dry
|
|
179
200
|
def message_text(template, tokens, options)
|
180
201
|
text = template[template.data(tokens)]
|
181
202
|
|
182
|
-
return text
|
203
|
+
return text if !text || !full
|
183
204
|
|
184
205
|
rule = options[:path]
|
185
206
|
"#{messages.rule(rule, options) || rule} #{text}"
|