dry-validation 0.8.0 → 0.9.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/.travis.yml +1 -1
- data/CHANGELOG.md +39 -1
- data/benchmarks/benchmark_schema_invalid_huge.rb +52 -0
- data/benchmarks/profile_schema_huge_invalid.rb +30 -0
- data/config/errors.yml +3 -2
- data/dry-validation.gemspec +2 -2
- data/lib/dry/validation.rb +20 -32
- data/lib/dry/validation/constants.rb +6 -0
- data/lib/dry/validation/error.rb +5 -2
- data/lib/dry/validation/error_compiler.rb +46 -116
- data/lib/dry/validation/executor.rb +105 -0
- data/lib/dry/validation/hint_compiler.rb +36 -68
- data/lib/dry/validation/message.rb +86 -0
- data/lib/dry/validation/message_compiler.rb +141 -0
- data/lib/dry/validation/message_set.rb +70 -0
- data/lib/dry/validation/messages/abstract.rb +1 -1
- data/lib/dry/validation/messages/i18n.rb +5 -0
- data/lib/dry/validation/predicate_registry.rb +8 -3
- data/lib/dry/validation/result.rb +6 -7
- data/lib/dry/validation/schema.rb +21 -227
- data/lib/dry/validation/schema/check.rb +1 -1
- data/lib/dry/validation/schema/class_interface.rb +193 -0
- data/lib/dry/validation/schema/deprecated.rb +1 -2
- data/lib/dry/validation/schema/key.rb +4 -0
- data/lib/dry/validation/schema/value.rb +12 -7
- data/lib/dry/validation/schema_compiler.rb +20 -1
- data/lib/dry/validation/type_specs.rb +70 -0
- data/lib/dry/validation/version.rb +1 -1
- data/spec/fixtures/locales/pl.yml +1 -1
- data/spec/integration/custom_predicates_spec.rb +37 -0
- data/spec/integration/error_compiler_spec.rb +39 -39
- data/spec/integration/form/predicates/key_spec.rb +10 -18
- data/spec/integration/form/predicates/size/fixed_spec.rb +8 -12
- data/spec/integration/form/predicates/size/range_spec.rb +7 -7
- data/spec/integration/hints_spec.rb +17 -0
- data/spec/integration/messages/i18n_spec.rb +2 -2
- data/spec/integration/schema/check_rules_spec.rb +2 -2
- data/spec/integration/schema/defining_base_schema_spec.rb +38 -0
- data/spec/integration/schema/dynamic_predicate_args_spec.rb +18 -0
- data/spec/integration/schema/macros/each_spec.rb +2 -2
- data/spec/integration/schema/macros/input_spec.rb +102 -10
- data/spec/integration/schema/macros/maybe_spec.rb +30 -0
- data/spec/integration/schema/nested_schemas_spec.rb +200 -0
- data/spec/integration/schema/nested_values_spec.rb +3 -1
- data/spec/integration/schema/option_with_default_spec.rb +54 -20
- data/spec/integration/schema/predicates/size/fixed_spec.rb +10 -10
- data/spec/integration/schema/predicates/size/range_spec.rb +8 -10
- data/spec/unit/error_compiler_spec.rb +1 -1
- data/spec/unit/hint_compiler_spec.rb +2 -2
- metadata +18 -7
- data/examples/rule_ast.rb +0 -25
- data/lib/dry/validation/error_compiler/input.rb +0 -135
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'dry/validation/constants'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Validation
|
5
|
+
class MessageSet
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
attr_reader :messages, :hints, :paths, :placeholders
|
9
|
+
|
10
|
+
def self.[](messages)
|
11
|
+
new(messages.flatten)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(messages)
|
15
|
+
@messages = messages
|
16
|
+
@hints = {}
|
17
|
+
@paths = map(&:path).uniq
|
18
|
+
initialize_placeholders!
|
19
|
+
end
|
20
|
+
|
21
|
+
def empty?
|
22
|
+
messages.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
def each(&block)
|
26
|
+
return to_enum unless block
|
27
|
+
messages.each(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_hints!(hints)
|
31
|
+
@hints = hints.group_by(&:index_path)
|
32
|
+
freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_h
|
36
|
+
reduce(placeholders) do |hash, msg|
|
37
|
+
if msg.root?
|
38
|
+
(hash[nil] ||= []) << msg.to_s
|
39
|
+
else
|
40
|
+
node = msg.path.reduce(hash) { |a, e| a[e] }
|
41
|
+
node << msg
|
42
|
+
node.concat(Array(hints[msg.index_path]))
|
43
|
+
node.uniq!(&:signature)
|
44
|
+
node.map!(&:to_s)
|
45
|
+
end
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
alias_method :to_hash, :to_h
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def initialize_placeholders!
|
54
|
+
@placeholders = paths.reduce({}) do |hash, path|
|
55
|
+
curr_idx = 0
|
56
|
+
last_idx = path.size - 1
|
57
|
+
node = hash
|
58
|
+
|
59
|
+
while curr_idx <= last_idx do
|
60
|
+
key = path[curr_idx]
|
61
|
+
node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
|
62
|
+
curr_idx += 1
|
63
|
+
end
|
64
|
+
|
65
|
+
hash
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -5,8 +5,11 @@ module Dry
|
|
5
5
|
attr_reader :external
|
6
6
|
|
7
7
|
class Bound < PredicateRegistry
|
8
|
+
attr_reader :schema
|
9
|
+
|
8
10
|
def initialize(*args)
|
9
|
-
super
|
11
|
+
super(*args[0..1])
|
12
|
+
@schema = args.last
|
10
13
|
freeze
|
11
14
|
end
|
12
15
|
end
|
@@ -16,7 +19,7 @@ module Dry
|
|
16
19
|
bound_predicates = predicates.each_with_object({}) do |(n, p), res|
|
17
20
|
res[n] = p.bind(schema)
|
18
21
|
end
|
19
|
-
Bound.new(external, bound_predicates)
|
22
|
+
Bound.new(external, bound_predicates, schema)
|
20
23
|
end
|
21
24
|
|
22
25
|
def update(other)
|
@@ -68,7 +71,9 @@ module Dry
|
|
68
71
|
predicates.key?(name) || external.key?(name)
|
69
72
|
end
|
70
73
|
|
71
|
-
def ensure_valid_predicate(name, args_or_arity)
|
74
|
+
def ensure_valid_predicate(name, args_or_arity, schema = nil)
|
75
|
+
return if schema && schema.instance_methods.include?(name)
|
76
|
+
|
72
77
|
if name == :key?
|
73
78
|
raise InvalidSchemaError, "#{name} is a reserved predicate name"
|
74
79
|
end
|
@@ -41,17 +41,16 @@ module Dry
|
|
41
41
|
@messages ||=
|
42
42
|
begin
|
43
43
|
return EMPTY_MESSAGES if success?
|
44
|
-
|
45
44
|
hints = hint_compiler.with(options).call
|
46
|
-
|
45
|
+
msg_set = error_compiler.with(options).(error_ast).with_hints!(hints)
|
47
46
|
|
48
|
-
|
49
|
-
msg_hash = comp.dump_messages(messages)
|
47
|
+
as_hash = options.fetch(:as_hash, true)
|
50
48
|
|
51
|
-
if
|
52
|
-
|
49
|
+
if as_hash
|
50
|
+
hash = msg_set.to_h
|
51
|
+
hash.key?(nil) ? hash.values.flatten : hash
|
53
52
|
else
|
54
|
-
|
53
|
+
msg_set
|
55
54
|
end
|
56
55
|
end
|
57
56
|
end
|
@@ -14,203 +14,15 @@ require 'dry/validation/error_compiler'
|
|
14
14
|
require 'dry/validation/hint_compiler'
|
15
15
|
|
16
16
|
require 'dry/validation/schema/deprecated'
|
17
|
+
require 'dry/validation/schema/class_interface'
|
18
|
+
require 'dry/validation/executor'
|
17
19
|
|
18
20
|
module Dry
|
19
21
|
module Validation
|
20
22
|
class Schema
|
21
|
-
|
23
|
+
attr_reader :config
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
setting :path
|
26
|
-
setting :predicates, Logic::Predicates
|
27
|
-
setting :registry
|
28
|
-
setting :messages, :yaml
|
29
|
-
setting :messages_file
|
30
|
-
setting :namespace
|
31
|
-
setting :rules, []
|
32
|
-
setting :checks, []
|
33
|
-
setting :options, {}
|
34
|
-
setting :type_map, {}
|
35
|
-
setting :hash_type, :weak
|
36
|
-
setting :input, nil
|
37
|
-
setting :dsl_extensions, nil
|
38
|
-
|
39
|
-
setting :input_processor, :noop
|
40
|
-
setting :input_processor_map, {
|
41
|
-
sanitizer: InputProcessorCompiler::Sanitizer.new,
|
42
|
-
json: InputProcessorCompiler::JSON.new,
|
43
|
-
form: InputProcessorCompiler::Form.new,
|
44
|
-
}.freeze
|
45
|
-
|
46
|
-
setting :type_specs, false
|
47
|
-
|
48
|
-
def self.inherited(klass)
|
49
|
-
super
|
50
|
-
klass.config.options = klass.config.options.dup
|
51
|
-
|
52
|
-
if registry && self != Schema
|
53
|
-
klass.config.registry = registry.new(self)
|
54
|
-
else
|
55
|
-
klass.set_registry!
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.clone
|
60
|
-
klass = Class.new(self)
|
61
|
-
klass.config.rules = []
|
62
|
-
klass.config.registry = registry
|
63
|
-
klass
|
64
|
-
end
|
65
|
-
|
66
|
-
def self.set_registry!
|
67
|
-
config.registry = PredicateRegistry[self, config.predicates]
|
68
|
-
end
|
69
|
-
|
70
|
-
def self.registry
|
71
|
-
config.registry
|
72
|
-
end
|
73
|
-
|
74
|
-
def self.build_array_type(spec, category)
|
75
|
-
member_schema = build_type_map(spec, category)
|
76
|
-
member_type = lookup_type("hash", category)
|
77
|
-
.public_send(config.hash_type, member_schema)
|
78
|
-
|
79
|
-
lookup_type("array", category).member(member_type)
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.build_type_map(type_specs, category = config.input_processor)
|
83
|
-
if type_specs.is_a?(Array)
|
84
|
-
build_array_type(type_specs[0], category)
|
85
|
-
else
|
86
|
-
type_specs.each_with_object({}) do |(name, spec), result|
|
87
|
-
result[name] =
|
88
|
-
case spec
|
89
|
-
when Hash
|
90
|
-
lookup_type("hash", category).public_send(config.hash_type, spec)
|
91
|
-
when Array
|
92
|
-
if spec.size == 1
|
93
|
-
if spec[0].is_a?(Hash)
|
94
|
-
build_array_type(spec[0], category)
|
95
|
-
else
|
96
|
-
lookup_type("array", category).member(lookup_type(spec[0], category))
|
97
|
-
end
|
98
|
-
else
|
99
|
-
spec
|
100
|
-
.map { |id| id.is_a?(Symbol) ? lookup_type(id, category) : id }
|
101
|
-
.reduce(:|)
|
102
|
-
end
|
103
|
-
when Symbol
|
104
|
-
lookup_type(spec, category)
|
105
|
-
else
|
106
|
-
spec
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def self.lookup_type(name, category)
|
113
|
-
id = "#{category}.#{name}"
|
114
|
-
Types.type_keys.include?(id) ? Types[id] : Types[name.to_s]
|
115
|
-
end
|
116
|
-
|
117
|
-
|
118
|
-
def self.type_map
|
119
|
-
config.type_map
|
120
|
-
end
|
121
|
-
|
122
|
-
def self.predicates(predicate_set = nil)
|
123
|
-
if predicate_set
|
124
|
-
config.predicates = predicate_set
|
125
|
-
set_registry!
|
126
|
-
else
|
127
|
-
config.predicates
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def self.new(rules = config.rules, **options)
|
132
|
-
super(rules, default_options.merge(options))
|
133
|
-
end
|
134
|
-
|
135
|
-
def self.create_class(target, other = nil, &block)
|
136
|
-
klass =
|
137
|
-
if other.is_a?(self)
|
138
|
-
Class.new(other.class)
|
139
|
-
elsif other.is_a?(Class) && other < Types::Struct
|
140
|
-
Validation.Schema(parent: target, build: false) do
|
141
|
-
other.schema.each { |attr, type| required(attr).filled(type) }
|
142
|
-
end
|
143
|
-
elsif other.respond_to?(:schema) && other.schema.is_a?(self)
|
144
|
-
Class.new(other.schema.class)
|
145
|
-
else
|
146
|
-
Validation.Schema(target.schema_class, parent: target, build: false, &block)
|
147
|
-
end
|
148
|
-
|
149
|
-
klass.config.path = target.path if other
|
150
|
-
klass.config.input_processor = :noop
|
151
|
-
|
152
|
-
klass
|
153
|
-
end
|
154
|
-
|
155
|
-
def self.option(name, default = nil)
|
156
|
-
attr_reader(*name)
|
157
|
-
options.update(name => default)
|
158
|
-
end
|
159
|
-
|
160
|
-
def self.to_ast
|
161
|
-
[:schema, self]
|
162
|
-
end
|
163
|
-
|
164
|
-
def self.rules
|
165
|
-
config.rules
|
166
|
-
end
|
167
|
-
|
168
|
-
def self.options
|
169
|
-
config.options
|
170
|
-
end
|
171
|
-
|
172
|
-
def self.messages
|
173
|
-
default = default_messages
|
174
|
-
|
175
|
-
if config.messages_file && config.namespace
|
176
|
-
default.merge(config.messages_file).namespaced(config.namespace)
|
177
|
-
elsif config.messages_file
|
178
|
-
default.merge(config.messages_file)
|
179
|
-
elsif config.namespace
|
180
|
-
default.namespaced(config.namespace)
|
181
|
-
else
|
182
|
-
default
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def self.default_messages
|
187
|
-
case config.messages
|
188
|
-
when :yaml then Messages.default
|
189
|
-
when :i18n then Messages::I18n.new
|
190
|
-
else
|
191
|
-
raise "+#{config.messages}+ is not a valid messages identifier"
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
def self.error_compiler
|
196
|
-
@error_compiler ||= ErrorCompiler.new(messages)
|
197
|
-
end
|
198
|
-
|
199
|
-
def self.hint_compiler
|
200
|
-
@hint_compiler ||= HintCompiler.new(messages, rules: rule_ast)
|
201
|
-
end
|
202
|
-
|
203
|
-
def self.rule_ast
|
204
|
-
@rule_ast ||= config.rules.map(&:to_ast)
|
205
|
-
end
|
206
|
-
|
207
|
-
def self.default_options
|
208
|
-
{ predicate_registry: registry,
|
209
|
-
error_compiler: error_compiler,
|
210
|
-
hint_compiler: hint_compiler,
|
211
|
-
input_processor: input_processor,
|
212
|
-
checks: config.checks }
|
213
|
-
end
|
25
|
+
attr_reader :input_rule
|
214
26
|
|
215
27
|
attr_reader :rules
|
216
28
|
|
@@ -230,18 +42,29 @@ module Dry
|
|
230
42
|
|
231
43
|
attr_reader :type_map
|
232
44
|
|
45
|
+
attr_reader :executor
|
46
|
+
|
233
47
|
def initialize(rules, options)
|
234
48
|
@type_map = self.class.type_map
|
49
|
+
@config = self.class.config
|
235
50
|
@predicates = options.fetch(:predicate_registry).bind(self)
|
236
|
-
@rule_compiler = SchemaCompiler.new(predicates)
|
51
|
+
@rule_compiler = SchemaCompiler.new(predicates, options)
|
237
52
|
@error_compiler = options.fetch(:error_compiler)
|
238
53
|
@hint_compiler = options.fetch(:hint_compiler)
|
239
|
-
@input_processor = options
|
54
|
+
@input_processor = options[:input_processor]
|
55
|
+
@input_rule = rule_compiler.visit(config.input_rule.to_ast) if config.input_rule
|
240
56
|
|
241
57
|
initialize_options(options)
|
242
58
|
initialize_rules(rules)
|
243
59
|
initialize_checks(options.fetch(:checks, []))
|
244
60
|
|
61
|
+
@executor = Executor.new(config.path) do |steps|
|
62
|
+
steps << ProcessInput.new(input_processor) if input_processor
|
63
|
+
steps << ApplyInputRule.new(input_rule) if input_rule
|
64
|
+
steps << ApplyRules.new(@rules)
|
65
|
+
steps << ApplyChecks.new(@checks) if @checks.any?
|
66
|
+
end
|
67
|
+
|
245
68
|
freeze
|
246
69
|
end
|
247
70
|
|
@@ -250,12 +73,12 @@ module Dry
|
|
250
73
|
end
|
251
74
|
|
252
75
|
def call(input)
|
253
|
-
|
254
|
-
Result.new(
|
76
|
+
output, result = executor.(input)
|
77
|
+
Result.new(output, result, error_compiler, hint_compiler)
|
255
78
|
end
|
256
79
|
|
257
|
-
def curry(*
|
258
|
-
|
80
|
+
def curry(*curry_args)
|
81
|
+
-> *args { call(*(curry_args + args)) }
|
259
82
|
end
|
260
83
|
|
261
84
|
def to_proc
|
@@ -272,35 +95,6 @@ module Dry
|
|
272
95
|
|
273
96
|
private
|
274
97
|
|
275
|
-
def apply(input)
|
276
|
-
results = rule_results(input)
|
277
|
-
|
278
|
-
results.merge!(check_results(input, results)) unless checks.empty?
|
279
|
-
|
280
|
-
results
|
281
|
-
.select { |_, result| result.failure? }
|
282
|
-
.map { |name, result| Error.new(error_path(name), result) }
|
283
|
-
end
|
284
|
-
|
285
|
-
def error_path(name)
|
286
|
-
full_path = Array[*self.class.config.path]
|
287
|
-
full_path << name
|
288
|
-
full_path.size > 1 ? full_path : full_path[0]
|
289
|
-
end
|
290
|
-
|
291
|
-
def rule_results(input)
|
292
|
-
rules.each_with_object({}) do |(name, rule), hash|
|
293
|
-
hash[name] = rule.(input)
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
def check_results(input, result)
|
298
|
-
checks.each_with_object({}) do |(name, check), hash|
|
299
|
-
check_res = check.is_a?(Guard) ? check.(input, result) : check.(input)
|
300
|
-
hash[name] = check_res if check_res
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
98
|
def initialize_options(options)
|
305
99
|
@options = options
|
306
100
|
|
@@ -23,7 +23,7 @@ module Dry
|
|
23
23
|
|
24
24
|
keys = [name, *vals.map(&:name)]
|
25
25
|
|
26
|
-
registry.ensure_valid_predicate(meth, args.size + keys.size)
|
26
|
+
registry.ensure_valid_predicate(meth, args.size + keys.size, schema_class)
|
27
27
|
predicate = registry[meth].curry(*args)
|
28
28
|
|
29
29
|
rule = create_rule([:check, [name, predicate.to_ast, keys]])
|