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,105 @@
1
+ module Dry
2
+ module Validation
3
+ class ProcessInput
4
+ attr_reader :processor
5
+
6
+ def initialize(processor)
7
+ @processor = processor
8
+ end
9
+
10
+ def call(input, *)
11
+ processor.(input)
12
+ end
13
+ end
14
+
15
+ class ApplyInputRule
16
+ attr_reader :rule
17
+
18
+ def initialize(rule)
19
+ @rule = rule
20
+ end
21
+
22
+ def call(input, result)
23
+ rule_res = rule.(input)
24
+ result.update(nil => rule_res) unless rule_res.success?
25
+ input
26
+ end
27
+ end
28
+
29
+ class ApplyRules
30
+ attr_reader :rules
31
+
32
+ def initialize(rules)
33
+ @rules = rules
34
+ end
35
+
36
+ def call(input, result)
37
+ rules.each_with_object(result) do |(name, rule), hash|
38
+ hash[name] = rule.(input)
39
+ end
40
+ input
41
+ end
42
+ end
43
+
44
+ class ApplyChecks < ApplyRules
45
+ def call(input, result)
46
+ rules.each_with_object(result) do |(name, check), hash|
47
+ check_res = check.is_a?(Guard) ? check.(input, result) : check.(input)
48
+ hash[name] = check_res if check_res
49
+ end
50
+ input
51
+ end
52
+ end
53
+
54
+ class BuildErrors
55
+ attr_reader :path
56
+
57
+ def self.[](path)
58
+ path.nil? || path.empty? ? Flat.new : Nested.new(path)
59
+ end
60
+
61
+ class Flat < BuildErrors
62
+ def error_path(name)
63
+ name
64
+ end
65
+ end
66
+
67
+ class Nested < BuildErrors
68
+ def initialize(path)
69
+ @path = path
70
+ end
71
+
72
+ def error_path(name)
73
+ [*path, name]
74
+ end
75
+ end
76
+
77
+ def call(result)
78
+ result
79
+ .select { |_, r| r.failure? }
80
+ .map { |name, r| Error.new(error_path(name), r) }
81
+ end
82
+ end
83
+
84
+ class Executor
85
+ attr_reader :steps, :final
86
+
87
+ def self.new(path, &block)
88
+ super(BuildErrors[path]).tap { |executor| yield(executor.steps) }.freeze
89
+ end
90
+
91
+ def initialize(final)
92
+ @steps = []
93
+ @final = final
94
+ end
95
+
96
+ def call(input, result = {})
97
+ output = steps.reduce(input) do |a, e|
98
+ return [a, final.(result)] if result.key?(nil)
99
+ e.call(a, result)
100
+ end
101
+ [output, final.(result)]
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,8 +1,8 @@
1
- require 'dry/validation/error_compiler/input'
1
+ require 'dry/validation/message_compiler'
2
2
 
3
3
  module Dry
4
4
  module Validation
5
- class HintCompiler < ErrorCompiler::Input
5
+ class HintCompiler < MessageCompiler
6
6
  include Dry::Equalizer(:messages, :rules, :options)
7
7
 
8
8
  attr_reader :rules, :excluded, :cache
@@ -21,115 +21,83 @@ module Dry
21
21
  array?: Array
22
22
  }.freeze
23
23
 
24
- EXCLUDED = [:none?, :filled?, :key?].freeze
25
-
26
- DEFAULT_OPTIONS = { name: nil, input: nil, message_type: :hint }.freeze
27
-
28
- EMPTY_MESSAGES = {}.freeze
24
+ EXCLUDED = (%i(key? none? filled?) + TYPES.keys).freeze
29
25
 
30
26
  def self.cache
31
27
  @cache ||= Concurrent::Map.new
32
28
  end
33
29
 
34
30
  def initialize(messages, options = {})
35
- super(messages, DEFAULT_OPTIONS.merge(options))
36
- @rules = @options.delete(:rules)
37
- @excluded = @options.fetch(:excluded, EXCLUDED)
38
- @val_type = options[:val_type]
31
+ super(messages, options)
32
+ @rules = options.fetch(:rules, EMPTY_ARRAY)
33
+ @excluded = options.fetch(:excluded, EXCLUDED)
39
34
  @cache = self.class.cache
40
35
  end
41
36
 
42
- def hash
43
- @hash ||= [messages, rules, options].hash
37
+ def message_type
38
+ :hint
44
39
  end
45
40
 
46
- def with(new_options)
47
- return self if new_options.empty?
48
- super(new_options.merge(rules: rules))
41
+ def message_class
42
+ Hint
49
43
  end
50
44
 
51
- def call
52
- cache.fetch_or_store(hash) { super(rules) }
53
- end
54
-
55
- def visit_predicate(node)
56
- predicate, _ = node
57
-
58
- val_type = TYPES[predicate]
59
-
60
- return with(val_type: val_type) if val_type
61
- return EMPTY_MESSAGES if excluded.include?(predicate)
62
-
63
- super
45
+ def hash
46
+ @hash ||= [messages, rules, options].hash
64
47
  end
65
48
 
66
- def visit_set(node)
67
- result = node.map do |el|
68
- visit(el)
69
- end
70
- merge(result)
49
+ def call
50
+ cache.fetch_or_store(hash) { super(rules) }
71
51
  end
72
52
 
73
- def visit_each(node)
74
- visit(node)
53
+ def visit_predicate(node, opts = EMPTY_HASH)
54
+ predicate, args = node
55
+ return EMPTY_ARRAY if excluded.include?(predicate) || dyn_args?(args)
56
+ super(node, opts.merge(val_type: TYPES[predicate]))
75
57
  end
76
58
 
77
- def visit_or(node)
78
- left, right = node
79
- merge([visit(left), visit(right)])
59
+ def visit_each(node, opts = EMPTY_HASH)
60
+ visit(node, opts.merge(each: true))
80
61
  end
81
62
 
82
- def visit_and(node)
63
+ def visit_or(node, *args)
83
64
  left, right = node
84
-
85
- result = visit(left)
86
-
87
- if result.is_a?(self.class)
88
- result.visit(right)
89
- else
90
- visit(right)
91
- end
65
+ [Array[visit(left, *args)], Array[visit(right, *args)]].flatten
92
66
  end
93
67
 
94
- def visit_implication(node)
68
+ def visit_and(node, *args)
95
69
  _, right = node
96
- visit(right)
70
+ visit(right, *args)
97
71
  end
98
72
 
99
- def visit_key(node)
100
- name, predicate = node
101
- with(name: Array([*self.name, name])).visit(predicate)
102
- end
103
- alias_method :visit_attr, :visit_key
104
-
105
- def visit_val(node)
106
- visit(node)
107
- end
73
+ def visit_schema(node, opts = EMPTY_HASH)
74
+ path = node.config.path
75
+ rules = node.rule_ast
76
+ schema_opts = opts.merge(path: [path])
108
77
 
109
- def visit_schema(node)
110
- merge(node.rule_ast.map(&method(:visit)))
78
+ rules.map { |rule| visit(rule, schema_opts) }
111
79
  end
112
80
 
113
81
  def visit_check(node)
114
- DEFAULT_RESULT
82
+ EMPTY_ARRAY
115
83
  end
116
84
 
117
85
  def visit_xor(node)
118
- DEFAULT_RESULT
86
+ EMPTY_ARRAY
119
87
  end
120
88
 
121
89
  def visit_not(node)
122
- DEFAULT_RESULT
90
+ EMPTY_ARRAY
123
91
  end
124
92
 
125
- def visit_type(node)
126
- visit(node.rule.to_ast)
93
+ def visit_type(node, *args)
94
+ visit(node.rule.to_ast, *args)
127
95
  end
128
96
 
129
97
  private
130
98
 
131
- def merge(result)
132
- super(result.reject { |el| el.is_a?(self.class) })
99
+ def dyn_args?(args)
100
+ args.map(&:last).any? { |a| a.is_a?(UnboundMethod) }
133
101
  end
134
102
  end
135
103
  end
@@ -0,0 +1,86 @@
1
+ require 'dry/validation/constants'
2
+
3
+ module Dry
4
+ module Validation
5
+ class Message
6
+ include Dry::Equalizer(:predicate, :path, :text, :options)
7
+
8
+ Index = Class.new {
9
+ def inspect
10
+ "index"
11
+ end
12
+ alias_method :to_s, :inspect
13
+ }.new
14
+
15
+ attr_reader :predicate, :path, :text, :rule, :args, :options
16
+
17
+ class Each < Message
18
+ def index_path
19
+ @index_path ||= [*path[0..path.size-2], Index]
20
+ end
21
+
22
+ def each?
23
+ true
24
+ end
25
+ end
26
+
27
+ def self.[](predicate, path, text, options)
28
+ klass = options[:each] ? Message::Each : Message
29
+ klass.new(predicate, path, text, options)
30
+ end
31
+
32
+ def initialize(predicate, path, text, options)
33
+ @predicate = predicate
34
+ @path = path
35
+ @text = text
36
+ @options = options
37
+ @rule = options[:rule]
38
+ @each = options[:each] || false
39
+ @args = options[:args] || EMPTY_ARRAY
40
+ end
41
+
42
+ alias_method :index_path, :path
43
+
44
+ def to_s
45
+ text
46
+ end
47
+
48
+ def signature
49
+ @signature ||= [predicate, args, index_path].hash
50
+ end
51
+
52
+ def each?
53
+ @each
54
+ end
55
+
56
+ def hint?
57
+ false
58
+ end
59
+
60
+ def root?
61
+ path.empty?
62
+ end
63
+
64
+ def eql?(other)
65
+ other.is_a?(String) ? text == other : super
66
+ end
67
+ end
68
+
69
+ class Hint < Message
70
+ def self.[](predicate, path, text, options)
71
+ klass = options[:each] ? Hint::Each : Hint
72
+ klass.new(predicate, path, text, options)
73
+ end
74
+
75
+ class Each < Hint
76
+ def index_path
77
+ @index_path ||= [*path, Index]
78
+ end
79
+ end
80
+
81
+ def hint?
82
+ true
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,141 @@
1
+ require 'dry/validation/constants'
2
+ require 'dry/validation/message'
3
+ require 'dry/validation/message_set'
4
+
5
+ module Dry
6
+ module Validation
7
+ class MessageCompiler
8
+ attr_reader :messages, :options, :locale, :default_lookup_options
9
+
10
+ def initialize(messages, options = {})
11
+ @messages = messages
12
+ @options = options
13
+ @full = @options.fetch(:full, false)
14
+ @locale = @options.fetch(:locale, :en)
15
+ @default_lookup_options = { message_type: message_type, locale: locale }
16
+ end
17
+
18
+ def call(ast)
19
+ MessageSet[ast.map { |node| visit(node) }]
20
+ end
21
+
22
+ def full?
23
+ @full
24
+ end
25
+
26
+ def with(new_options)
27
+ return self if new_options.empty?
28
+ self.class.new(messages, options.merge(new_options))
29
+ end
30
+
31
+ def visit(node, *args)
32
+ __send__(:"visit_#{node[0]}", node[1], *args)
33
+ end
34
+
35
+ def visit_predicate(node, base_opts = EMPTY_HASH)
36
+ predicate, args = node
37
+
38
+ *arg_vals, _ = args.map(&:last)
39
+
40
+ tokens = message_tokens(args)
41
+
42
+ if base_opts[:message] == false
43
+ return [predicate, arg_vals, tokens]
44
+ end
45
+
46
+ options = base_opts.update(lookup_options(base_opts, arg_vals))
47
+ msg_opts = options.update(tokens)
48
+
49
+ name = msg_opts[:name]
50
+ rule = msg_opts[:rule] || name
51
+
52
+ template = messages[predicate, msg_opts]
53
+
54
+ unless template
55
+ raise MissingMessageError, "message for #{predicate} was not found"
56
+ end
57
+
58
+ text = message_text(rule, template, tokens, options)
59
+ path = message_path(msg_opts, name)
60
+
61
+ message_class[
62
+ predicate, path, text,
63
+ args: arg_vals, rule: rule, each: base_opts[:each] == true
64
+ ]
65
+ end
66
+
67
+ def visit_key(node, opts = EMPTY_HASH)
68
+ name, predicate = node
69
+ visit(predicate, opts.merge(name: name))
70
+ end
71
+
72
+ def visit_val(node, opts = EMPTY_HASH)
73
+ visit(node, opts)
74
+ end
75
+
76
+ def visit_set(node, opts = EMPTY_HASH)
77
+ node.map { |input| visit(input, opts) }
78
+ end
79
+
80
+ def visit_el(node, opts = EMPTY_HASH)
81
+ idx, el = node
82
+ visit(el, opts.merge(path: opts[:path] + [idx]))
83
+ end
84
+
85
+ def visit_implication(node, *args)
86
+ _, right = node
87
+ visit(right, *args)
88
+ end
89
+
90
+ def visit_xor(node, *args)
91
+ _, right = node
92
+ visit(right, *args)
93
+ end
94
+
95
+ def lookup_options(_opts, arg_vals = [])
96
+ default_lookup_options.merge(
97
+ arg_type: arg_vals.size == 1 && arg_vals[0].class
98
+ )
99
+ end
100
+
101
+ def message_text(rule, template, tokens, opts)
102
+ text = template % tokens
103
+
104
+ if full?
105
+ rule_name = messages.rule(rule, opts) || rule
106
+ "#{rule_name} #{text}"
107
+ else
108
+ text
109
+ end
110
+ end
111
+
112
+ def message_path(opts, name)
113
+ if name.is_a?(Array)
114
+ name
115
+ else
116
+ path = opts[:path] || Array(name)
117
+
118
+ if name && path.last != name
119
+ path += [name]
120
+ end
121
+
122
+ path
123
+ end
124
+ end
125
+
126
+ def message_tokens(args)
127
+ args.each_with_object({}) { |arg, hash|
128
+ case arg[1]
129
+ when Array
130
+ hash[arg[0]] = arg[1].join(', ')
131
+ when Range
132
+ hash["#{arg[0]}_left".to_sym] = arg[1].first
133
+ hash["#{arg[0]}_right".to_sym] = arg[1].last
134
+ else
135
+ hash[arg[0]] = arg[1]
136
+ end
137
+ }
138
+ end
139
+ end
140
+ end
141
+ end