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.
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]])