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
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'dry/validation/messages/i18n'
|
2
|
+
|
3
|
+
RSpec.describe 'Validation hints' do
|
4
|
+
subject(:validation) { schema.new }
|
5
|
+
|
6
|
+
shared_context '#messages' do
|
7
|
+
it 'provides hints for additional rules that were not checked' do
|
8
|
+
expect(validation.(age: '17').messages).to eql(
|
9
|
+
age: [['age must be an integer', 'age must be greater than 18'], '17']
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'with yaml messages' do
|
15
|
+
let(:schema) do
|
16
|
+
Class.new(Dry::Validation::Schema) do
|
17
|
+
key(:age) do |age|
|
18
|
+
age.none? | (age.int? & age.gt?(18))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
include_context '#messages'
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with i18n messages' do
|
27
|
+
let(:schema) do
|
28
|
+
Class.new(Dry::Validation::Schema) do
|
29
|
+
configure { |c| c.messages = :i18n }
|
30
|
+
|
31
|
+
key(:age) do |age|
|
32
|
+
age.none? | (age.int? & age.gt?(18))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
include_context '#messages'
|
38
|
+
end
|
39
|
+
end
|
@@ -24,7 +24,7 @@ RSpec.describe Dry::Validation, 'with localized messages' do
|
|
24
24
|
describe '#messages' do
|
25
25
|
it 'returns localized error messages' do
|
26
26
|
expect(validation.(email: '').messages(locale: :pl)).to match_array([
|
27
|
-
[:email, [['Proszę podać adres email', '']]
|
27
|
+
[:email, [['Proszę podać adres email'], '']]
|
28
28
|
])
|
29
29
|
end
|
30
30
|
end
|
@@ -45,7 +45,7 @@ RSpec.describe Dry::Validation, 'with localized messages' do
|
|
45
45
|
describe '#messages' do
|
46
46
|
it 'returns localized error messages' do
|
47
47
|
expect(validation.(email: '').messages(locale: :pl)).to match_array([
|
48
|
-
[:email, [['Hej user! Dawaj ten email no!', '']]
|
48
|
+
[:email, [['Hej user! Dawaj ten email no!'], '']]
|
49
49
|
])
|
50
50
|
end
|
51
51
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
RSpec.describe Schema, 'using generic rules' do
|
2
|
+
subject(:validate) { schema.new }
|
3
|
+
|
4
|
+
let(:schema) do
|
5
|
+
Class.new(Schema) do
|
6
|
+
def self.messages
|
7
|
+
Messages.default.merge(
|
8
|
+
en: { errors: { destiny: 'you must select either red or blue' } }
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
optional(:red, &:filled?)
|
13
|
+
optional(:blue, &:filled?)
|
14
|
+
|
15
|
+
rule(:destiny) { rule(:red) | rule(:blue) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'passes when only red is filled' do
|
20
|
+
expect(validate.(red: '1')).to be_empty
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'fails when red and blue are not filled ' do
|
24
|
+
expect(validate.(red: '', blue: '').messages[:destiny]).to eql(
|
25
|
+
[['you must select either red or blue'], '']
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
RSpec.describe 'Schema with each and set rules' do
|
2
|
+
subject(:validation) { schema.new }
|
3
|
+
|
4
|
+
let(:schema) do
|
5
|
+
Class.new(Dry::Validation::Schema) do
|
6
|
+
key(:payments) do |payments|
|
7
|
+
payments.array? do
|
8
|
+
payments.each do |payment|
|
9
|
+
payment.key(:method, &:str?)
|
10
|
+
payment.key(:amount, &:float?)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#messages' do
|
18
|
+
it 'validates each payment against its set of rules' do
|
19
|
+
input = {
|
20
|
+
payments: [
|
21
|
+
{ method: 'cc', amount: 1.23 },
|
22
|
+
{ method: 'wire', amount: 4.56 }
|
23
|
+
]
|
24
|
+
}
|
25
|
+
|
26
|
+
expect(validation.(input).messages).to eql({})
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'validates presence of the method key for each payment' do
|
30
|
+
input = {
|
31
|
+
payments: [
|
32
|
+
{ method: 'cc', amount: 1.23 },
|
33
|
+
{ amount: 4.56 }
|
34
|
+
]
|
35
|
+
}
|
36
|
+
|
37
|
+
expect(validation.(input).messages[:payments]).to eql([
|
38
|
+
[payments: [[method: [["method is missing"], nil]], input[:payments][1]]],
|
39
|
+
input[:payments]
|
40
|
+
])
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'validates type of the method value for each payment' do
|
44
|
+
input = {
|
45
|
+
payments: [
|
46
|
+
{ method: 'cc', amount: 1.23 },
|
47
|
+
{ method: 12, amount: 4.56 }
|
48
|
+
]
|
49
|
+
}
|
50
|
+
|
51
|
+
expect(validation.(input).messages[:payments]).to eql([
|
52
|
+
[payments: [[method: [["method must be a string"], 12]], input[:payments][1]]],
|
53
|
+
input[:payments]
|
54
|
+
])
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'validates type of the amount value for each payment' do
|
58
|
+
input = {
|
59
|
+
payments: [
|
60
|
+
{ method: 'cc', amount: 1.23 },
|
61
|
+
{ method: 'wire', amount: '4.56' }
|
62
|
+
]
|
63
|
+
}
|
64
|
+
|
65
|
+
expect(validation.(input).messages[:payments]).to eql([
|
66
|
+
[payments: [[amount: [["amount must be a float"], '4.56']], input[:payments][1]]],
|
67
|
+
input[:payments]
|
68
|
+
])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
RSpec.describe Schema, 'using nested schemas' do
|
2
|
+
subject(:validate) { schema.new }
|
3
|
+
|
4
|
+
let(:schema) do
|
5
|
+
Class.new(Schema) do
|
6
|
+
schema(:location) do |loc|
|
7
|
+
loc.key(:lat, &:filled?)
|
8
|
+
loc.key(:lng, &:filled?)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'passes when location has lat and lng filled' do
|
14
|
+
expect(validate.(location: { lat: 1.23, lng: 4.56 })).to be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'fails when location has missing lat' do
|
18
|
+
expect(validate.(location: { lng: 4.56 })).to match_array([
|
19
|
+
[
|
20
|
+
:error, [
|
21
|
+
:input, [
|
22
|
+
:location, { lng: 4.56 },
|
23
|
+
[
|
24
|
+
[:input, [:lat, nil, [[:key, [:lat, [:predicate, [:key?, [:lat]]]]]]]]
|
25
|
+
]
|
26
|
+
]
|
27
|
+
]
|
28
|
+
]
|
29
|
+
])
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
RSpec.describe 'Schema with negated rules' do
|
2
|
+
subject(:validate) { schema.new }
|
3
|
+
|
4
|
+
let(:schema) do
|
5
|
+
Class.new(Dry::Validation::Schema) do
|
6
|
+
def self.messages
|
7
|
+
Messages.default.merge(
|
8
|
+
en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
optional(:eat_cake) { |v| v.eql?('yes!') }
|
13
|
+
optional(:have_cake) { |v| v.eql?('yes!') }
|
14
|
+
|
15
|
+
rule(:be_reasonable) do
|
16
|
+
rule(:eat_cake) & rule(:have_cake).not
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#messages' do
|
22
|
+
it 'passes when only one option is selected' do
|
23
|
+
messages = validate.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
|
24
|
+
|
25
|
+
expect(messages).to be(nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'fails when both options are selected' do
|
29
|
+
messages = validate.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
|
30
|
+
|
31
|
+
expect(messages).to eql([['you cannot eat cake and have cake!'], 'yes!'])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
RSpec.describe 'Schema with xor rules' do
|
2
|
+
subject(:validate) { schema.new }
|
3
|
+
|
4
|
+
let(:schema) do
|
5
|
+
Class.new(Dry::Validation::Schema) do
|
6
|
+
def self.messages
|
7
|
+
Messages.default.merge(
|
8
|
+
en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
key(:eat_cake) { |v| v.eql?('yes!') }
|
13
|
+
key(:have_cake) { |v| v.eql?('yes!') }
|
14
|
+
|
15
|
+
rule(:be_reasonable) { rule(:eat_cake) ^ rule(:have_cake) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#messages' do
|
20
|
+
it 'passes when only one option is selected' do
|
21
|
+
messages = validate.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
|
22
|
+
|
23
|
+
expect(messages).to be(nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'fails when both options are selected' do
|
27
|
+
messages = validate.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
|
28
|
+
|
29
|
+
expect(messages).to eql([['you cannot eat cake and have cake!'], 'yes!'])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -33,8 +33,8 @@ RSpec.describe Dry::Validation::Schema::Form do
|
|
33
33
|
result = validation.('email' => '', 'age' => '19')
|
34
34
|
|
35
35
|
expect(result.messages).to match_array([
|
36
|
-
[:email, [['email must be filled', '']]
|
37
|
-
[:address, [['address is missing', nil]]
|
36
|
+
[:email, [['email must be filled'], '']],
|
37
|
+
[:address, [['address is missing'], nil]]
|
38
38
|
])
|
39
39
|
|
40
40
|
expect(result.params).to eql(email: '', age: 19)
|
@@ -47,7 +47,7 @@ RSpec.describe Dry::Validation::Schema do
|
|
47
47
|
describe '#messages' do
|
48
48
|
it 'returns compiled error messages' do
|
49
49
|
expect(validation.(attrs.merge(email: '')).messages).to match_array([
|
50
|
-
[:email, [['email must be filled', '']]
|
50
|
+
[:email, [['email must be filled'], '']]
|
51
51
|
])
|
52
52
|
end
|
53
53
|
end
|
data/spec/shared/predicates.rb
CHANGED
@@ -5,6 +5,8 @@ RSpec.shared_examples 'predicates' do
|
|
5
5
|
|
6
6
|
let(:str?) { Dry::Validation::Predicates[:str?] }
|
7
7
|
|
8
|
+
let(:filled?) { Dry::Validation::Predicates[:filled?] }
|
9
|
+
|
8
10
|
let(:min_size?) { Dry::Validation::Predicates[:min_size?] }
|
9
11
|
|
10
12
|
let(:key?) { Dry::Validation::Predicates[:key?] }
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'dry/validation/hint_compiler'
|
2
|
+
|
3
|
+
RSpec.describe HintCompiler, '#call' do
|
4
|
+
subject(:compiler) { HintCompiler.new(Messages.default, rules: rules) }
|
5
|
+
|
6
|
+
let(:rules) do
|
7
|
+
[
|
8
|
+
[
|
9
|
+
:and, [
|
10
|
+
[:key, [:age, [:predicate, [:key?, []]]]],
|
11
|
+
[
|
12
|
+
:or, [
|
13
|
+
[:val, [:age, [:predicate, [:none?, []]]]],
|
14
|
+
[
|
15
|
+
:and, [
|
16
|
+
[:val, [:age, [:predicate, [:int?, []]]]],
|
17
|
+
[:val, [:age, [:predicate, [:gt?, [18]]]]]
|
18
|
+
]
|
19
|
+
]
|
20
|
+
]
|
21
|
+
]
|
22
|
+
]
|
23
|
+
]
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns hint messages for given rules' do
|
28
|
+
expect(compiler.call).to eql(
|
29
|
+
age: ['age must be an integer', 'age must be greater than 18']
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
data/spec/unit/predicate_spec.rb
CHANGED
@@ -11,16 +11,6 @@ RSpec.describe Dry::Validation::Predicate do
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
describe '#negation' do
|
15
|
-
it 'returns a negated version of a predicate' do
|
16
|
-
is_empty = Dry::Validation::Predicate.new(:is_empty) { |str| str.empty? }
|
17
|
-
is_filled = is_empty.negation
|
18
|
-
|
19
|
-
expect(is_filled.('')).to be(false)
|
20
|
-
expect(is_filled.('filled')).to be(true)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
14
|
describe '#curry' do
|
25
15
|
it 'returns curried predicate' do
|
26
16
|
min_age = Dry::Validation::Predicate.new(:min_age) { |age, input| input >= age }
|
@@ -0,0 +1,29 @@
|
|
1
|
+
RSpec.describe Rule::Check do
|
2
|
+
subject(:rule) { Rule::Check.new(:name, other.(input).curry(predicate)) }
|
3
|
+
|
4
|
+
include_context 'predicates'
|
5
|
+
|
6
|
+
let(:other) do
|
7
|
+
Rule::Value.new(:name, none?).or(Rule::Value.new(:name, filled?))
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#call' do
|
11
|
+
context 'when a given predicate passed' do
|
12
|
+
let(:input) { 'Jane' }
|
13
|
+
let(:predicate) { :filled? }
|
14
|
+
|
15
|
+
it 'returns a success' do
|
16
|
+
expect(rule.()).to be_success
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when a given predicate did not pass' do
|
21
|
+
let(:input) { nil }
|
22
|
+
let(:predicate) { :filled? }
|
23
|
+
|
24
|
+
it 'returns a failure' do
|
25
|
+
expect(rule.()).to be_failure
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,15 +4,20 @@ RSpec.describe Dry::Validation::RuleCompiler, '#call' do
|
|
4
4
|
subject(:compiler) { RuleCompiler.new(predicates) }
|
5
5
|
|
6
6
|
let(:predicates) {
|
7
|
-
{ key?: predicate,
|
7
|
+
{ key?: predicate,
|
8
|
+
filled?: predicate,
|
9
|
+
email: val_rule.('email').curry(:filled?) }
|
8
10
|
}
|
9
11
|
|
10
12
|
let(:predicate) { double(:predicate).as_null_object }
|
11
13
|
|
12
14
|
let(:key_rule) { Rule::Key.new(:email, predicate) }
|
15
|
+
let(:not_key_rule) { Rule::Key.new(:email, predicate).negation }
|
13
16
|
let(:val_rule) { Rule::Value.new(:email, predicate) }
|
17
|
+
let(:check_rule) { Rule::Check.new(:email, predicates[:email]) }
|
14
18
|
let(:and_rule) { key_rule & val_rule }
|
15
19
|
let(:or_rule) { key_rule | val_rule }
|
20
|
+
let(:xor_rule) { key_rule ^ val_rule }
|
16
21
|
let(:set_rule) { Rule::Set.new(:email, [val_rule]) }
|
17
22
|
let(:each_rule) { Rule::Each.new(:email, val_rule) }
|
18
23
|
|
@@ -24,12 +29,29 @@ RSpec.describe Dry::Validation::RuleCompiler, '#call' do
|
|
24
29
|
expect(rules).to eql([key_rule])
|
25
30
|
end
|
26
31
|
|
32
|
+
it 'compiles check rules' do
|
33
|
+
ast = [[:check, [:email, [:predicate, [:email, [:filled?]]]]]]
|
34
|
+
|
35
|
+
rules = compiler.(ast)
|
36
|
+
|
37
|
+
expect(rules).to eql([check_rule])
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
it 'compiles negated rules' do
|
42
|
+
ast = [[:not, [:key, [:email, [:predicate, [:key?, predicate]]]]]]
|
43
|
+
|
44
|
+
rules = compiler.(ast)
|
45
|
+
|
46
|
+
expect(rules).to eql([not_key_rule])
|
47
|
+
end
|
48
|
+
|
27
49
|
it 'compiles conjunction rules' do
|
28
50
|
ast = [
|
29
51
|
[
|
30
52
|
:and, [
|
31
|
-
[:key, [:email, [:predicate, [:key?,
|
32
|
-
[:val, [:email, [:predicate, [:filled?,
|
53
|
+
[:key, [:email, [:predicate, [:key?, []]]]],
|
54
|
+
[:val, [:email, [:predicate, [:filled?, []]]]]
|
33
55
|
]
|
34
56
|
]
|
35
57
|
]
|
@@ -43,8 +65,8 @@ RSpec.describe Dry::Validation::RuleCompiler, '#call' do
|
|
43
65
|
ast = [
|
44
66
|
[
|
45
67
|
:or, [
|
46
|
-
[:key, [:email, [:predicate, [:key?,
|
47
|
-
[:val, [:email, [:predicate, [:filled?,
|
68
|
+
[:key, [:email, [:predicate, [:key?, []]]]],
|
69
|
+
[:val, [:email, [:predicate, [:filled?, []]]]]
|
48
70
|
]
|
49
71
|
]
|
50
72
|
]
|
@@ -54,12 +76,27 @@ RSpec.describe Dry::Validation::RuleCompiler, '#call' do
|
|
54
76
|
expect(rules).to eql([or_rule])
|
55
77
|
end
|
56
78
|
|
79
|
+
it 'compiles exclusive disjunction rules' do
|
80
|
+
ast = [
|
81
|
+
[
|
82
|
+
:xor, [
|
83
|
+
[:key, [:email, [:predicate, [:key?, []]]]],
|
84
|
+
[:val, [:email, [:predicate, [:filled?, []]]]]
|
85
|
+
]
|
86
|
+
]
|
87
|
+
]
|
88
|
+
|
89
|
+
rules = compiler.(ast)
|
90
|
+
|
91
|
+
expect(rules).to eql([xor_rule])
|
92
|
+
end
|
93
|
+
|
57
94
|
it 'compiles set rules' do
|
58
95
|
ast = [
|
59
96
|
[
|
60
97
|
:set, [
|
61
98
|
:email, [
|
62
|
-
[:val, [:email, [:predicate, [:filled?,
|
99
|
+
[:val, [:email, [:predicate, [:filled?, []]]]]
|
63
100
|
]
|
64
101
|
]
|
65
102
|
]
|
@@ -74,7 +111,7 @@ RSpec.describe Dry::Validation::RuleCompiler, '#call' do
|
|
74
111
|
ast = [
|
75
112
|
[
|
76
113
|
:each, [
|
77
|
-
:email, [:val, [:email, [:predicate, [:filled?,
|
114
|
+
:email, [:val, [:email, [:predicate, [:filled?, []]]]]
|
78
115
|
]
|
79
116
|
]
|
80
117
|
]
|