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,35 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/logic/predicates"
|
4
|
+
require "dry/types/predicate_registry"
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Schema
|
7
8
|
# A registry with predicate objects from `Dry::Logic::Predicates`
|
8
9
|
#
|
9
10
|
# @api private
|
10
|
-
class PredicateRegistry
|
11
|
-
# @api private
|
12
|
-
attr_reader :predicates
|
13
|
-
|
14
|
-
# @api private
|
15
|
-
attr_reader :has_predicate
|
16
|
-
|
17
|
-
# @api private
|
18
|
-
def initialize(predicates = Dry::Logic::Predicates)
|
19
|
-
@predicates = predicates
|
20
|
-
@has_predicate = ::Kernel.instance_method(:respond_to?).bind(@predicates)
|
21
|
-
end
|
22
|
-
|
23
|
-
# @api private
|
24
|
-
def [](name)
|
25
|
-
predicates[name]
|
26
|
-
end
|
27
|
-
|
28
|
-
# @api private
|
29
|
-
def key?(name)
|
30
|
-
has_predicate.(name)
|
31
|
-
end
|
32
|
-
|
11
|
+
class PredicateRegistry < Dry::Types::PredicateRegistry
|
33
12
|
# @api private
|
34
13
|
def arg_list(name, *values)
|
35
14
|
predicate = self[name]
|
@@ -1,99 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/types/primitive_inferrer"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Schema
|
7
|
-
# PrimitiveInferrer is used internally by `Macros::Filled`
|
8
|
-
# for inferring a list of possible primitives that a given
|
9
|
-
# type can handle.
|
10
|
-
#
|
11
7
|
# @api private
|
12
|
-
class PrimitiveInferrer
|
13
|
-
|
8
|
+
class PrimitiveInferrer < ::Dry::Types::PrimitiveInferrer
|
9
|
+
Compiler = ::Class.new(superclass::Compiler)
|
14
10
|
|
15
|
-
# Compiler reduces type AST into a list of primitives
|
16
|
-
#
|
17
|
-
# @api private
|
18
|
-
class Compiler
|
19
|
-
# @api private
|
20
|
-
def visit(node)
|
21
|
-
meth, rest = node
|
22
|
-
public_send(:"visit_#{meth}", rest)
|
23
|
-
end
|
24
|
-
|
25
|
-
# @api private
|
26
|
-
def visit_nominal(node)
|
27
|
-
type, _ = node
|
28
|
-
type
|
29
|
-
end
|
30
|
-
|
31
|
-
# @api private
|
32
|
-
def visit_hash(_)
|
33
|
-
Hash
|
34
|
-
end
|
35
|
-
alias_method :visit_schema, :visit_hash
|
36
|
-
|
37
|
-
# @api private
|
38
|
-
def visit_array(_)
|
39
|
-
Array
|
40
|
-
end
|
41
|
-
|
42
|
-
# @api private
|
43
|
-
def visit_lax(node)
|
44
|
-
visit(node)
|
45
|
-
end
|
46
|
-
|
47
|
-
# @api private
|
48
|
-
def visit_constructor(node)
|
49
|
-
other, * = node
|
50
|
-
visit(other)
|
51
|
-
end
|
52
|
-
|
53
|
-
# @api private
|
54
|
-
def visit_enum(node)
|
55
|
-
other, * = node
|
56
|
-
visit(other)
|
57
|
-
end
|
58
|
-
|
59
|
-
# @api private
|
60
|
-
def visit_sum(node)
|
61
|
-
left, right = node
|
62
|
-
|
63
|
-
[visit(left), visit(right)].flatten(1)
|
64
|
-
end
|
65
|
-
|
66
|
-
# @api private
|
67
|
-
def visit_constrained(node)
|
68
|
-
other, * = node
|
69
|
-
visit(other)
|
70
|
-
end
|
71
|
-
|
72
|
-
# @api private
|
73
|
-
def visit_any(_)
|
74
|
-
Object
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# @return [Compiler]
|
79
|
-
# @api private
|
80
|
-
attr_reader :compiler
|
81
|
-
|
82
|
-
# @api private
|
83
11
|
def initialize
|
84
12
|
@compiler = Compiler.new
|
85
13
|
end
|
86
|
-
|
87
|
-
# Infer predicate identifier from the provided type
|
88
|
-
#
|
89
|
-
# @return [Symbol]
|
90
|
-
#
|
91
|
-
# @api private
|
92
|
-
def [](type)
|
93
|
-
self.class.fetch_or_store(type.hash) do
|
94
|
-
Array(compiler.visit(type.to_ast)).freeze
|
95
|
-
end
|
96
|
-
end
|
97
14
|
end
|
98
15
|
end
|
99
16
|
end
|
data/lib/dry/schema/processor.rb
CHANGED
@@ -1,25 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/configurable"
|
4
|
+
require "dry/initializer"
|
5
|
+
require "dry/logic/operators"
|
5
6
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
7
|
+
require "dry/schema/type_registry"
|
8
|
+
require "dry/schema/type_container"
|
9
|
+
require "dry/schema/processor_steps"
|
10
|
+
require "dry/schema/rule_applier"
|
11
|
+
require "dry/schema/key_coercer"
|
12
|
+
require "dry/schema/value_coercer"
|
11
13
|
|
12
14
|
module Dry
|
13
15
|
module Schema
|
14
16
|
# Processes input data using objects configured within the DSL
|
17
|
+
# Processing is split into steps represented by `ProcessorSteps`.
|
15
18
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# 1. Prepare input hash using a key map
|
19
|
-
# 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
|
20
|
-
# 3. Apply value coercions based on type specifications
|
21
|
-
# 4. Apply rules
|
22
|
-
#
|
19
|
+
# @see ProcessorSteps
|
23
20
|
# @see Params
|
24
21
|
# @see JSON
|
25
22
|
#
|
@@ -28,11 +25,13 @@ module Dry
|
|
28
25
|
extend Dry::Initializer
|
29
26
|
extend Dry::Configurable
|
30
27
|
|
28
|
+
include Dry::Logic::Operators
|
29
|
+
|
31
30
|
setting :key_map_type
|
32
31
|
setting :type_registry_namespace, :strict
|
33
32
|
setting :filter_empty_string, false
|
34
33
|
|
35
|
-
option :steps, default: -> {
|
34
|
+
option :steps, default: -> { ProcessorSteps.new }
|
36
35
|
|
37
36
|
option :schema_dsl
|
38
37
|
|
@@ -66,27 +65,17 @@ module Dry
|
|
66
65
|
# @api public
|
67
66
|
def new(options = nil, &block)
|
68
67
|
if options || block
|
69
|
-
processor = super
|
68
|
+
processor = super(**(options || EMPTY_HASH))
|
70
69
|
yield(processor) if block
|
71
70
|
processor
|
72
71
|
elsif definition
|
73
72
|
definition.call
|
74
73
|
else
|
75
|
-
raise ArgumentError,
|
74
|
+
raise ArgumentError, "Cannot create a schema without a definition"
|
76
75
|
end
|
77
76
|
end
|
78
77
|
end
|
79
78
|
|
80
|
-
# Append a step
|
81
|
-
#
|
82
|
-
# @return [Processor]
|
83
|
-
#
|
84
|
-
# @api private
|
85
|
-
def <<(step)
|
86
|
-
steps << step
|
87
|
-
self
|
88
|
-
end
|
89
|
-
|
90
79
|
# Apply processing steps to the provided input
|
91
80
|
#
|
92
81
|
# @param [Hash] input
|
@@ -95,31 +84,36 @@ module Dry
|
|
95
84
|
#
|
96
85
|
# @api public
|
97
86
|
def call(input)
|
98
|
-
Result.new(input, message_compiler: message_compiler) do |result|
|
99
|
-
steps.
|
100
|
-
output = step.(result)
|
101
|
-
result.replace(output) if output.is_a?(::Hash)
|
102
|
-
end
|
87
|
+
Result.new(input, input: input, message_compiler: message_compiler) do |result|
|
88
|
+
steps.call(result)
|
103
89
|
end
|
104
90
|
end
|
105
91
|
alias_method :[], :call
|
106
92
|
|
107
|
-
#
|
93
|
+
# @api public
|
94
|
+
def xor(other)
|
95
|
+
raise NotImplementedError, "composing schemas using `xor` operator is not supported yet"
|
96
|
+
end
|
97
|
+
alias_method :^, :xor
|
98
|
+
|
99
|
+
# Merge with another schema
|
108
100
|
#
|
109
|
-
# @
|
101
|
+
# @param [Processor] other
|
102
|
+
#
|
103
|
+
# @return [Processor, Params, JSON]
|
110
104
|
#
|
111
105
|
# @api public
|
112
|
-
def
|
113
|
-
|
106
|
+
def merge(other)
|
107
|
+
schema_dsl.merge(other.schema_dsl).()
|
114
108
|
end
|
115
109
|
|
116
|
-
# Return
|
110
|
+
# Return a proc that acts like a schema object
|
117
111
|
#
|
118
|
-
# @return [
|
112
|
+
# @return [Proc]
|
119
113
|
#
|
120
114
|
# @api public
|
121
|
-
def
|
122
|
-
|
115
|
+
def to_proc
|
116
|
+
->(input) { call(input) }
|
123
117
|
end
|
124
118
|
|
125
119
|
# Return string represntation
|
@@ -133,15 +127,32 @@ module Dry
|
|
133
127
|
STR
|
134
128
|
end
|
135
129
|
|
130
|
+
# Return the key map
|
131
|
+
#
|
132
|
+
# @return [KeyMap]
|
133
|
+
#
|
134
|
+
# @api public
|
135
|
+
def key_map
|
136
|
+
steps.key_map
|
137
|
+
end
|
138
|
+
|
136
139
|
# Return the type schema
|
137
140
|
#
|
138
141
|
# @return [Dry::Types::Safe]
|
139
142
|
#
|
140
143
|
# @api private
|
141
144
|
def type_schema
|
142
|
-
|
145
|
+
steps.type_schema
|
143
146
|
end
|
144
147
|
|
148
|
+
# Return the rule applier
|
149
|
+
#
|
150
|
+
# @api private
|
151
|
+
def rule_applier
|
152
|
+
steps.rule_applier
|
153
|
+
end
|
154
|
+
alias_method :to_rule, :rule_applier
|
155
|
+
|
145
156
|
# Return the rules config
|
146
157
|
#
|
147
158
|
# @return [Dry::Types::Config]
|
@@ -154,9 +165,10 @@ module Dry
|
|
154
165
|
# Return AST representation of the rules
|
155
166
|
#
|
156
167
|
# @api private
|
157
|
-
def to_ast
|
168
|
+
def to_ast(*)
|
158
169
|
rule_applier.to_ast
|
159
170
|
end
|
171
|
+
alias_method :ast, :to_ast
|
160
172
|
|
161
173
|
# Return the message compiler
|
162
174
|
#
|
@@ -176,14 +188,6 @@ module Dry
|
|
176
188
|
rule_applier.rules
|
177
189
|
end
|
178
190
|
|
179
|
-
# Return the rule applier
|
180
|
-
#
|
181
|
-
# @api private
|
182
|
-
def rule_applier
|
183
|
-
@rule_applier ||= steps.last
|
184
|
-
end
|
185
|
-
alias_method :to_rule, :rule_applier
|
186
|
-
|
187
191
|
# Check if there are filter rules
|
188
192
|
#
|
189
193
|
# @api private
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/initializer"
|
4
|
+
|
5
|
+
require "dry/schema/constants"
|
6
|
+
require "dry/schema/step"
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Schema
|
10
|
+
# Steps for the Dry::Schema::Processor
|
11
|
+
#
|
12
|
+
# There are 4 main steps:
|
13
|
+
#
|
14
|
+
# 1. `key_coercer` - Prepare input hash using a key map
|
15
|
+
# 2. `filter_schema` - Apply pre-coercion filtering rules
|
16
|
+
# (optional step, used only when `filter` was used)
|
17
|
+
# 3. `value_coercer` - Apply value coercions based on type specifications
|
18
|
+
# 4. `rule_applier` - Apply rules
|
19
|
+
#
|
20
|
+
# @see Processor
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
class ProcessorSteps
|
24
|
+
extend Dry::Initializer
|
25
|
+
|
26
|
+
option :steps, default: -> { EMPTY_HASH.dup }
|
27
|
+
option :before_steps, default: -> { EMPTY_HASH.dup }
|
28
|
+
option :after_steps, default: -> { EMPTY_HASH.dup }
|
29
|
+
|
30
|
+
# Executes steps and callbacks in order
|
31
|
+
#
|
32
|
+
# @param [Result] result
|
33
|
+
#
|
34
|
+
# @return [Result]
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def call(result)
|
38
|
+
STEPS_IN_ORDER.each do |name|
|
39
|
+
before_steps[name]&.each { |step| step&.(result) }
|
40
|
+
steps[name]&.(result)
|
41
|
+
after_steps[name]&.each { |step| step&.(result) }
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
# @api private
|
48
|
+
def rule_applier
|
49
|
+
@rule_applier ||= steps[:rule_applier].executor
|
50
|
+
end
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def key_map
|
54
|
+
@key_map ||= self[:key_coercer].executor.key_map
|
55
|
+
end
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
def type_schema
|
59
|
+
@type_schema ||= steps[:value_coercer].executor.type_schema
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns step by name
|
63
|
+
#
|
64
|
+
# @param [Symbol] name The step name
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
def [](name)
|
68
|
+
steps[name]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sets step by name
|
72
|
+
#
|
73
|
+
# @param [Symbol] name The step name
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def []=(name, value)
|
77
|
+
steps[name] = Step.new(type: :core, name: name, executor: value)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Add passed block before mentioned step
|
81
|
+
#
|
82
|
+
# @param [Symbol] name The step name
|
83
|
+
#
|
84
|
+
# @return [ProcessorSteps]
|
85
|
+
#
|
86
|
+
# @api public
|
87
|
+
def after(name, &block)
|
88
|
+
after_steps[name] ||= EMPTY_ARRAY.dup
|
89
|
+
after_steps[name] << Step.new(type: :after, name: name, executor: block)
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
# Add passed block before mentioned step
|
94
|
+
#
|
95
|
+
# @param [Symbol] name The step name
|
96
|
+
#
|
97
|
+
# @return [ProcessorSteps]
|
98
|
+
#
|
99
|
+
# @api public
|
100
|
+
def before(name, &block)
|
101
|
+
before_steps[name] ||= EMPTY_ARRAY.dup
|
102
|
+
before_steps[name] << Step.new(type: :before, name: name, executor: block)
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Stacks callback steps and returns new ProcessorSteps instance
|
107
|
+
#
|
108
|
+
# @param [ProcessorSteps] other
|
109
|
+
#
|
110
|
+
# @return [ProcessorSteps]
|
111
|
+
#
|
112
|
+
# @api public
|
113
|
+
def merge(other)
|
114
|
+
ProcessorSteps.new(
|
115
|
+
before_steps: merge_callbacks(before_steps, other.before_steps),
|
116
|
+
after_steps: merge_callbacks(after_steps, other.after_steps)
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
# @api private
|
121
|
+
def merge_callbacks(left, right)
|
122
|
+
left.merge(right) do |_key, oldval, newval|
|
123
|
+
oldval + newval
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @api private
|
128
|
+
def import_callbacks(path, other)
|
129
|
+
other.before_steps.each do |name, steps|
|
130
|
+
(before_steps[name] ||= []).concat(steps.map { |step| step.scoped(path) })
|
131
|
+
end
|
132
|
+
|
133
|
+
other.after_steps.each do |name, steps|
|
134
|
+
(after_steps[name] ||= []).concat(steps.map { |step| step.scoped(path) })
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|