dry-schema 1.3.4 → 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 +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}"
|