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
@@ -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
@@ -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
@@ -1,11 +1,11 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'dry-validation'
4
-
5
3
  begin
6
4
  require 'byebug'
7
5
  rescue LoadError; end
8
6
 
7
+ require 'dry-validation'
8
+
9
9
  SPEC_ROOT = Pathname(__dir__)
10
10
 
11
11
  Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:require))
@@ -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
@@ -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, filled?: 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?, predicate]]]],
32
- [:val, [:email, [:predicate, [:filled?, predicate]]]]
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?, predicate]]]],
47
- [:val, [:email, [:predicate, [:filled?, predicate]]]]
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?, predicate]]]]
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?, predicate]]]]
114
+ :email, [:val, [:email, [:predicate, [:filled?, []]]]]
78
115
  ]
79
116
  ]
80
117
  ]