dry-validation 0.3.1 → 0.4.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 (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)