dry-validation 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +35 -499
  4. data/lib/dry/validation/error_compiler.rb +9 -4
  5. data/lib/dry/validation/hint_compiler.rb +69 -0
  6. data/lib/dry/validation/predicate.rb +0 -4
  7. data/lib/dry/validation/result.rb +8 -0
  8. data/lib/dry/validation/rule.rb +44 -0
  9. data/lib/dry/validation/rule/check.rb +15 -0
  10. data/lib/dry/validation/rule/composite.rb +20 -7
  11. data/lib/dry/validation/rule/result.rb +46 -0
  12. data/lib/dry/validation/rule_compiler.rb +14 -0
  13. data/lib/dry/validation/schema.rb +33 -3
  14. data/lib/dry/validation/schema/definition.rb +25 -4
  15. data/lib/dry/validation/schema/key.rb +8 -8
  16. data/lib/dry/validation/schema/result.rb +15 -2
  17. data/lib/dry/validation/schema/rule.rb +32 -5
  18. data/lib/dry/validation/schema/value.rb +15 -6
  19. data/lib/dry/validation/version.rb +1 -1
  20. data/spec/integration/custom_error_messages_spec.rb +1 -1
  21. data/spec/integration/error_compiler_spec.rb +30 -56
  22. data/spec/integration/hints_spec.rb +39 -0
  23. data/spec/integration/localized_error_messages_spec.rb +2 -2
  24. data/spec/integration/schema/check_rules_spec.rb +28 -0
  25. data/spec/integration/schema/each_with_set_spec.rb +71 -0
  26. data/spec/integration/schema/nested_spec.rb +31 -0
  27. data/spec/integration/schema/not_spec.rb +34 -0
  28. data/spec/integration/schema/xor_spec.rb +32 -0
  29. data/spec/integration/schema_form_spec.rb +2 -2
  30. data/spec/integration/schema_spec.rb +1 -1
  31. data/spec/shared/predicates.rb +2 -0
  32. data/spec/spec_helper.rb +2 -2
  33. data/spec/unit/hint_compiler_spec.rb +32 -0
  34. data/spec/unit/predicate_spec.rb +0 -10
  35. data/spec/unit/rule/check_spec.rb +29 -0
  36. data/spec/unit/rule_compiler_spec.rb +44 -7
  37. data/spec/unit/schema/rule_spec.rb +31 -0
  38. data/spec/unit/schema/value_spec.rb +84 -0
  39. metadata +24 -2
@@ -14,8 +14,8 @@ module Dry
14
14
  ast.map { |node| visit(node) }.reduce(:merge) || DEFAULT_RESULT
15
15
  end
16
16
 
17
- def with(options)
18
- self.class.new(messages, options)
17
+ def with(new_options)
18
+ self.class.new(messages, options.merge(new_options))
19
19
  end
20
20
 
21
21
  def visit(node, *args)
@@ -28,7 +28,12 @@ module Dry
28
28
 
29
29
  def visit_input(input, *args)
30
30
  name, value, rules = input
31
- { name => rules.map { |rule| visit(rule, name, value) } }
31
+ { name => [rules.map { |rule| visit(rule, name, value) }, value] }
32
+ end
33
+
34
+ def visit_check(node, *args)
35
+ name, _ = node
36
+ messages[name, rule: name]
32
37
  end
33
38
 
34
39
  def visit_key(rule, name, value)
@@ -51,7 +56,7 @@ module Dry
51
56
  template = messages[predicate_name, lookup_options]
52
57
  tokens = visit(predicate, value).merge(name: name)
53
58
 
54
- [template % tokens, value]
59
+ template % tokens
55
60
  end
56
61
 
57
62
  def visit_key?(*args, value)
@@ -0,0 +1,69 @@
1
+ require 'dry/validation/error_compiler'
2
+
3
+ module Dry
4
+ module Validation
5
+ class HintCompiler < ErrorCompiler
6
+ attr_reader :messages, :rules, :options
7
+
8
+ def initialize(messages, options = {})
9
+ @messages = messages
10
+ @options = Hash[options]
11
+ @rules = @options.delete(:rules)
12
+ end
13
+
14
+ def with(new_options)
15
+ super(new_options.merge(rules: rules))
16
+ end
17
+
18
+ def call
19
+ messages = Hash.new { |h, k| h[k] = [] }
20
+
21
+ rules.map { |node| visit(node) }.compact.each do |hints|
22
+ name, msgs = hints
23
+ messages[name].concat(msgs)
24
+ end
25
+
26
+ messages
27
+ end
28
+
29
+ def visit_or(node)
30
+ left, right = node
31
+ [visit(left), Array(visit(right)).flatten.compact].compact
32
+ end
33
+
34
+ def visit_and(node)
35
+ left, right = node
36
+ [visit(left), Array(visit(right)).flatten.compact].compact
37
+ end
38
+
39
+ def visit_val(node)
40
+ name, predicate = node
41
+ visit(predicate, name)
42
+ end
43
+
44
+ def visit_predicate(node, name)
45
+ predicate_name, args = node
46
+
47
+ lookup_options = options.merge(rule: name, arg_type: args[0].class)
48
+
49
+ template = messages[predicate_name, lookup_options]
50
+ predicate_opts = visit(node, args)
51
+
52
+ return unless predicate_opts
53
+
54
+ tokens = predicate_opts.merge(name: name)
55
+
56
+ template % tokens
57
+ end
58
+
59
+ def visit_key(node)
60
+ name, _ = node
61
+ name
62
+ end
63
+
64
+ def method_missing(name, *args)
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -22,10 +22,6 @@ module Dry
22
22
  fn.(*args)
23
23
  end
24
24
 
25
- def negation
26
- self.class.new(:"not_#{id}") { |input| !fn.(input) }
27
- end
28
-
29
25
  def curry(*args)
30
26
  self.class.new(id, *args, &fn.curry.(*args))
31
27
  end
@@ -13,6 +13,14 @@ module Dry
13
13
  rule_results.each(&block)
14
14
  end
15
15
 
16
+ def to_h
17
+ each_with_object({}) { |result, hash| hash[result.name] = result }
18
+ end
19
+
20
+ def merge!(other)
21
+ rule_results.concat(other.rule_results)
22
+ end
23
+
16
24
  def to_ary
17
25
  failures.map(&:to_ary)
18
26
  end
@@ -5,11 +5,41 @@ module Dry
5
5
 
6
6
  attr_reader :name, :predicate
7
7
 
8
+ class Negation < Rule
9
+ include Dry::Equalizer(:rule)
10
+
11
+ attr_reader :rule
12
+
13
+ def initialize(rule)
14
+ @rule = rule
15
+ end
16
+
17
+ def call(*args)
18
+ rule.(*args).negated
19
+ end
20
+
21
+ def to_ary
22
+ [:not, rule.to_ary]
23
+ end
24
+ end
25
+
8
26
  def initialize(name, predicate)
9
27
  @name = name
10
28
  @predicate = predicate
11
29
  end
12
30
 
31
+ def predicate_id
32
+ predicate.id
33
+ end
34
+
35
+ def type
36
+ :rule
37
+ end
38
+
39
+ def call(*args)
40
+ Validation.Result(args, predicate.call, self)
41
+ end
42
+
13
43
  def to_ary
14
44
  [type, [name, predicate.to_ary]]
15
45
  end
@@ -25,11 +55,24 @@ module Dry
25
55
  end
26
56
  alias_method :|, :or
27
57
 
58
+ def xor(other)
59
+ ExclusiveDisjunction.new(self, other)
60
+ end
61
+ alias_method :^, :xor
62
+
28
63
  def then(other)
29
64
  Implication.new(self, other)
30
65
  end
31
66
  alias_method :>, :then
32
67
 
68
+ def negation
69
+ Negation.new(self)
70
+ end
71
+
72
+ def new(predicate)
73
+ self.class.new(name, predicate)
74
+ end
75
+
33
76
  def curry(*args)
34
77
  self.class.new(name, predicate.curry(*args))
35
78
  end
@@ -42,5 +85,6 @@ require 'dry/validation/rule/value'
42
85
  require 'dry/validation/rule/each'
43
86
  require 'dry/validation/rule/set'
44
87
  require 'dry/validation/rule/composite'
88
+ require 'dry/validation/rule/check'
45
89
  require 'dry/validation/rule/group'
46
90
  require 'dry/validation/rule/result'
@@ -0,0 +1,15 @@
1
+ module Dry
2
+ module Validation
3
+ class Rule::Check < Rule
4
+ alias_method :result, :predicate
5
+
6
+ def call(*)
7
+ Validation.Result(nil, result.call, self)
8
+ end
9
+
10
+ def type
11
+ :check
12
+ end
13
+ end
14
+ end
15
+ end
@@ -6,11 +6,14 @@ module Dry
6
6
  attr_reader :name, :left, :right
7
7
 
8
8
  def initialize(left, right)
9
- @name = left.name
10
9
  @left = left
11
10
  @right = right
12
11
  end
13
12
 
13
+ def name
14
+ :"#{left.name}_#{type}_#{right.name}"
15
+ end
16
+
14
17
  def to_ary
15
18
  [type, [left.to_ary, right.to_ary]]
16
19
  end
@@ -18,8 +21,8 @@ module Dry
18
21
  end
19
22
 
20
23
  class Rule::Implication < Rule::Composite
21
- def call(input)
22
- left.(input) > right
24
+ def call(*args)
25
+ left.(*args) > right
23
26
  end
24
27
 
25
28
  def type
@@ -28,8 +31,8 @@ module Dry
28
31
  end
29
32
 
30
33
  class Rule::Conjunction < Rule::Composite
31
- def call(input)
32
- left.(input).and(right)
34
+ def call(*args)
35
+ left.(*args).and(right)
33
36
  end
34
37
 
35
38
  def type
@@ -38,13 +41,23 @@ module Dry
38
41
  end
39
42
 
40
43
  class Rule::Disjunction < Rule::Composite
41
- def call(input)
42
- left.(input).or(right)
44
+ def call(*args)
45
+ left.(*args).or(right)
43
46
  end
44
47
 
45
48
  def type
46
49
  :or
47
50
  end
48
51
  end
52
+
53
+ class Rule::ExclusiveDisjunction < Rule::Composite
54
+ def call(*args)
55
+ left.(*args).xor(right)
56
+ end
57
+
58
+ def type
59
+ :xor
60
+ end
61
+ end
49
62
  end
50
63
  end
@@ -2,6 +2,7 @@ module Dry
2
2
  module Validation
3
3
  def self.Result(input, value, rule)
4
4
  case value
5
+ when Rule::Result then value.class.new(value.input, value.success?, rule)
5
6
  when Array then Rule::Result::Set.new(input, value, rule)
6
7
  else Rule::Result::Value.new(input, value, rule)
7
8
  end
@@ -30,6 +31,31 @@ module Dry
30
31
  alias_method :to_a, :to_ary
31
32
  end
32
33
 
34
+ class Rule::Result::Verified < Rule::Result
35
+ attr_reader :predicate_id
36
+
37
+ def initialize(result, predicate_id)
38
+ @input = result.input
39
+ @value = result.value
40
+ @rule = result.rule
41
+ @name = result.name
42
+ @predicate_id = predicate_id
43
+ end
44
+
45
+ def call
46
+ Validation.Result(input, success?, rule)
47
+ end
48
+
49
+ def to_ary
50
+ [:input, [name, input, [rule.to_ary]]]
51
+ end
52
+ alias_method :to_a, :to_ary
53
+
54
+ def success?
55
+ rule.predicate_id == predicate_id
56
+ end
57
+ end
58
+
33
59
  def initialize(input, value, rule)
34
60
  @input = input
35
61
  @value = value
@@ -37,6 +63,22 @@ module Dry
37
63
  @name = rule.name
38
64
  end
39
65
 
66
+ def call
67
+ self
68
+ end
69
+
70
+ def curry(predicate_id = nil)
71
+ if predicate_id
72
+ Rule::Result::Verified.new(self, predicate_id)
73
+ else
74
+ self
75
+ end
76
+ end
77
+
78
+ def negated
79
+ self.class.new(input, !value, rule)
80
+ end
81
+
40
82
  def >(other)
41
83
  if success?
42
84
  other.(input)
@@ -61,6 +103,10 @@ module Dry
61
103
  end
62
104
  end
63
105
 
106
+ def xor(other)
107
+ Validation.Result(input, success? ^ other.(input).success?, rule)
108
+ end
109
+
64
110
  def success?
65
111
  @value
66
112
  end
@@ -18,6 +18,15 @@ module Dry
18
18
  send(:"visit_#{name}", nodes)
19
19
  end
20
20
 
21
+ def visit_check(node)
22
+ name, predicate = node
23
+ Rule::Check.new(name, visit(predicate))
24
+ end
25
+
26
+ def visit_not(node)
27
+ visit(node).negation
28
+ end
29
+
21
30
  def visit_key(node)
22
31
  name, predicate = node
23
32
  Rule::Key.new(name, visit(predicate))
@@ -53,6 +62,11 @@ module Dry
53
62
  visit(left) | visit(right)
54
63
  end
55
64
 
65
+ def visit_xor(node)
66
+ left, right = node
67
+ visit(left) ^ visit(right)
68
+ end
69
+
56
70
  def visit_implication(node)
57
71
  left, right = node
58
72
  visit(left) > visit(right)
@@ -4,6 +4,7 @@ require 'dry/validation/error'
4
4
  require 'dry/validation/rule_compiler'
5
5
  require 'dry/validation/messages'
6
6
  require 'dry/validation/error_compiler'
7
+ require 'dry/validation/hint_compiler'
7
8
  require 'dry/validation/result'
8
9
  require 'dry/validation/schema/result'
9
10
 
@@ -26,6 +27,10 @@ module Dry
26
27
  ErrorCompiler.new(messages)
27
28
  end
28
29
 
30
+ def self.hint_compiler
31
+ HintCompiler.new(messages, rules: rules.map(&:to_ary))
32
+ end
33
+
29
34
  def self.messages
30
35
  default =
31
36
  case config.messages
@@ -50,24 +55,49 @@ module Dry
50
55
  @__rules__ ||= []
51
56
  end
52
57
 
58
+ def self.schemas
59
+ @__schemas__ ||= []
60
+ end
61
+
53
62
  def self.groups
54
63
  @__groups__ ||= []
55
64
  end
56
65
 
57
- attr_reader :rules, :groups
66
+ def self.checks
67
+ @__checks__ ||= []
68
+ end
69
+
70
+ attr_reader :rules, :schemas, :groups, :checks
58
71
 
59
72
  attr_reader :error_compiler
60
73
 
61
- def initialize(error_compiler = self.class.error_compiler)
74
+ attr_reader :hint_compiler
75
+
76
+ def initialize(error_compiler = self.class.error_compiler, hint_compiler = self.class.hint_compiler)
62
77
  compiler = RuleCompiler.new(self)
63
78
  @rules = compiler.(self.class.rules.map(&:to_ary))
79
+ @checks = self.class.checks
64
80
  @groups = compiler.(self.class.groups.map(&:to_ary))
81
+ @schemas = self.class.schemas.map(&:new)
65
82
  @error_compiler = error_compiler
83
+ @hint_compiler = hint_compiler
66
84
  end
67
85
 
68
86
  def call(input)
69
87
  result = Validation::Result.new(rules.map { |rule| rule.(input) })
70
88
 
89
+ schemas.each do |schema|
90
+ result.merge!(schema.(input).result)
91
+ end
92
+
93
+ if checks.size > 0
94
+ compiled_checks = RuleCompiler.new(result.to_h).(checks)
95
+
96
+ compiled_checks.each do |rule|
97
+ result << rule.()
98
+ end
99
+ end
100
+
71
101
  groups.each do |group|
72
102
  result.with_values(group.rules) do |values|
73
103
  result << group.(*values)
@@ -76,7 +106,7 @@ module Dry
76
106
 
77
107
  errors = Error::Set.new(result.failures.map { |failure| Error.new(failure) })
78
108
 
79
- Schema::Result.new(input, result, errors, error_compiler)
109
+ Schema::Result.new(input, result, errors, error_compiler, hint_compiler)
80
110
  end
81
111
 
82
112
  def [](name)