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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +35 -499
- data/lib/dry/validation/error_compiler.rb +9 -4
- data/lib/dry/validation/hint_compiler.rb +69 -0
- data/lib/dry/validation/predicate.rb +0 -4
- data/lib/dry/validation/result.rb +8 -0
- data/lib/dry/validation/rule.rb +44 -0
- data/lib/dry/validation/rule/check.rb +15 -0
- data/lib/dry/validation/rule/composite.rb +20 -7
- data/lib/dry/validation/rule/result.rb +46 -0
- data/lib/dry/validation/rule_compiler.rb +14 -0
- data/lib/dry/validation/schema.rb +33 -3
- data/lib/dry/validation/schema/definition.rb +25 -4
- data/lib/dry/validation/schema/key.rb +8 -8
- data/lib/dry/validation/schema/result.rb +15 -2
- data/lib/dry/validation/schema/rule.rb +32 -5
- data/lib/dry/validation/schema/value.rb +15 -6
- data/lib/dry/validation/version.rb +1 -1
- data/spec/integration/custom_error_messages_spec.rb +1 -1
- data/spec/integration/error_compiler_spec.rb +30 -56
- data/spec/integration/hints_spec.rb +39 -0
- data/spec/integration/localized_error_messages_spec.rb +2 -2
- data/spec/integration/schema/check_rules_spec.rb +28 -0
- data/spec/integration/schema/each_with_set_spec.rb +71 -0
- data/spec/integration/schema/nested_spec.rb +31 -0
- data/spec/integration/schema/not_spec.rb +34 -0
- data/spec/integration/schema/xor_spec.rb +32 -0
- data/spec/integration/schema_form_spec.rb +2 -2
- data/spec/integration/schema_spec.rb +1 -1
- data/spec/shared/predicates.rb +2 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/unit/hint_compiler_spec.rb +32 -0
- data/spec/unit/predicate_spec.rb +0 -10
- data/spec/unit/rule/check_spec.rb +29 -0
- data/spec/unit/rule_compiler_spec.rb +44 -7
- data/spec/unit/schema/rule_spec.rb +31 -0
- data/spec/unit/schema/value_spec.rb +84 -0
- 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(
|
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
|
-
|
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
|
@@ -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
|
data/lib/dry/validation/rule.rb
CHANGED
@@ -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'
|
@@ -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(
|
22
|
-
left.(
|
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(
|
32
|
-
left.(
|
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(
|
42
|
-
left.(
|
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
|
-
|
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
|
-
|
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)
|