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