dry-validation 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +39 -1
  4. data/benchmarks/benchmark_schema_invalid_huge.rb +52 -0
  5. data/benchmarks/profile_schema_huge_invalid.rb +30 -0
  6. data/config/errors.yml +3 -2
  7. data/dry-validation.gemspec +2 -2
  8. data/lib/dry/validation.rb +20 -32
  9. data/lib/dry/validation/constants.rb +6 -0
  10. data/lib/dry/validation/error.rb +5 -2
  11. data/lib/dry/validation/error_compiler.rb +46 -116
  12. data/lib/dry/validation/executor.rb +105 -0
  13. data/lib/dry/validation/hint_compiler.rb +36 -68
  14. data/lib/dry/validation/message.rb +86 -0
  15. data/lib/dry/validation/message_compiler.rb +141 -0
  16. data/lib/dry/validation/message_set.rb +70 -0
  17. data/lib/dry/validation/messages/abstract.rb +1 -1
  18. data/lib/dry/validation/messages/i18n.rb +5 -0
  19. data/lib/dry/validation/predicate_registry.rb +8 -3
  20. data/lib/dry/validation/result.rb +6 -7
  21. data/lib/dry/validation/schema.rb +21 -227
  22. data/lib/dry/validation/schema/check.rb +1 -1
  23. data/lib/dry/validation/schema/class_interface.rb +193 -0
  24. data/lib/dry/validation/schema/deprecated.rb +1 -2
  25. data/lib/dry/validation/schema/key.rb +4 -0
  26. data/lib/dry/validation/schema/value.rb +12 -7
  27. data/lib/dry/validation/schema_compiler.rb +20 -1
  28. data/lib/dry/validation/type_specs.rb +70 -0
  29. data/lib/dry/validation/version.rb +1 -1
  30. data/spec/fixtures/locales/pl.yml +1 -1
  31. data/spec/integration/custom_predicates_spec.rb +37 -0
  32. data/spec/integration/error_compiler_spec.rb +39 -39
  33. data/spec/integration/form/predicates/key_spec.rb +10 -18
  34. data/spec/integration/form/predicates/size/fixed_spec.rb +8 -12
  35. data/spec/integration/form/predicates/size/range_spec.rb +7 -7
  36. data/spec/integration/hints_spec.rb +17 -0
  37. data/spec/integration/messages/i18n_spec.rb +2 -2
  38. data/spec/integration/schema/check_rules_spec.rb +2 -2
  39. data/spec/integration/schema/defining_base_schema_spec.rb +38 -0
  40. data/spec/integration/schema/dynamic_predicate_args_spec.rb +18 -0
  41. data/spec/integration/schema/macros/each_spec.rb +2 -2
  42. data/spec/integration/schema/macros/input_spec.rb +102 -10
  43. data/spec/integration/schema/macros/maybe_spec.rb +30 -0
  44. data/spec/integration/schema/nested_schemas_spec.rb +200 -0
  45. data/spec/integration/schema/nested_values_spec.rb +3 -1
  46. data/spec/integration/schema/option_with_default_spec.rb +54 -20
  47. data/spec/integration/schema/predicates/size/fixed_spec.rb +10 -10
  48. data/spec/integration/schema/predicates/size/range_spec.rb +8 -10
  49. data/spec/unit/error_compiler_spec.rb +1 -1
  50. data/spec/unit/hint_compiler_spec.rb +2 -2
  51. metadata +18 -7
  52. data/examples/rule_ast.rb +0 -25
  53. 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
@@ -98,7 +98,7 @@ module Dry
98
98
  end
99
99
 
100
100
  def cache
101
- self.class.cache[self]
101
+ @cache ||= self.class.cache[self]
102
102
  end
103
103
  end
104
104
  end
@@ -20,6 +20,11 @@ module Dry
20
20
  def key?(key, options)
21
21
  ::I18n.exists?(key, options.fetch(:locale, I18n.default_locale))
22
22
  end
23
+
24
+ def merge(path)
25
+ ::I18n.load_path << path
26
+ self
27
+ end
23
28
  end
24
29
  end
25
30
  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
- comp = error_compiler.with(options.merge(hints: hints))
45
+ msg_set = error_compiler.with(options).(error_ast).with_hints!(hints)
47
46
 
48
- messages = comp.(error_ast)
49
- msg_hash = comp.dump_messages(messages)
47
+ as_hash = options.fetch(:as_hash, true)
50
48
 
51
- if msg_hash.key?(nil)
52
- msg_hash.values.flatten
49
+ if as_hash
50
+ hash = msg_set.to_h
51
+ hash.key?(nil) ? hash.values.flatten : hash
53
52
  else
54
- msg_hash
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
- extend Dry::Configurable
23
+ attr_reader :config
22
24
 
23
- NOOP_INPUT_PROCESSOR = -> input { input }
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.fetch(:input_processor, NOOP_INPUT_PROCESSOR)
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
- processed_input = input_processor[input]
254
- Result.new(processed_input, apply(processed_input), error_compiler, hint_compiler)
76
+ output, result = executor.(input)
77
+ Result.new(output, result, error_compiler, hint_compiler)
255
78
  end
256
79
 
257
- def curry(*args)
258
- to_proc.curry.(*args)
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]])