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.
- 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
|
]
|