dry-validation 0.6.0 → 0.7.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.travis.yml +3 -2
  4. data/CHANGELOG.md +42 -0
  5. data/Gemfile +8 -1
  6. data/README.md +13 -89
  7. data/config/errors.yml +35 -29
  8. data/dry-validation.gemspec +2 -2
  9. data/examples/basic.rb +3 -7
  10. data/examples/each.rb +3 -8
  11. data/examples/form.rb +3 -6
  12. data/examples/nested.rb +7 -15
  13. data/lib/dry/validation.rb +33 -5
  14. data/lib/dry/validation/error.rb +10 -26
  15. data/lib/dry/validation/error_compiler.rb +69 -99
  16. data/lib/dry/validation/error_compiler/input.rb +148 -0
  17. data/lib/dry/validation/hint_compiler.rb +83 -33
  18. data/lib/dry/validation/input_processor_compiler.rb +98 -0
  19. data/lib/dry/validation/input_processor_compiler/form.rb +46 -0
  20. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +46 -0
  21. data/lib/dry/validation/messages/abstract.rb +30 -10
  22. data/lib/dry/validation/messages/i18n.rb +2 -1
  23. data/lib/dry/validation/messages/namespaced.rb +1 -0
  24. data/lib/dry/validation/messages/yaml.rb +8 -5
  25. data/lib/dry/validation/result.rb +33 -25
  26. data/lib/dry/validation/schema.rb +168 -61
  27. data/lib/dry/validation/schema/attr.rb +5 -27
  28. data/lib/dry/validation/schema/check.rb +24 -0
  29. data/lib/dry/validation/schema/dsl.rb +97 -0
  30. data/lib/dry/validation/schema/form.rb +2 -26
  31. data/lib/dry/validation/schema/key.rb +32 -28
  32. data/lib/dry/validation/schema/rule.rb +88 -32
  33. data/lib/dry/validation/schema/value.rb +77 -27
  34. data/lib/dry/validation/schema_compiler.rb +38 -0
  35. data/lib/dry/validation/version.rb +1 -1
  36. data/spec/fixtures/locales/pl.yml +1 -1
  37. data/spec/integration/attr_spec.rb +122 -0
  38. data/spec/integration/custom_error_messages_spec.rb +9 -11
  39. data/spec/integration/custom_predicates_spec.rb +68 -18
  40. data/spec/integration/error_compiler_spec.rb +259 -65
  41. data/spec/integration/hints_spec.rb +28 -9
  42. data/spec/integration/injecting_rules_spec.rb +11 -12
  43. data/spec/integration/localized_error_messages_spec.rb +16 -16
  44. data/spec/integration/messages/i18n_spec.rb +9 -5
  45. data/spec/integration/optional_keys_spec.rb +9 -11
  46. data/spec/integration/schema/array_schema_spec.rb +23 -0
  47. data/spec/integration/schema/check_rules_spec.rb +39 -31
  48. data/spec/integration/schema/check_with_nth_el_spec.rb +25 -0
  49. data/spec/integration/schema/each_with_set_spec.rb +23 -24
  50. data/spec/integration/schema/form_spec.rb +122 -0
  51. data/spec/integration/schema/inheriting_schema_spec.rb +31 -0
  52. data/spec/integration/schema/input_processor_spec.rb +46 -0
  53. data/spec/integration/schema/macros/confirmation_spec.rb +33 -0
  54. data/spec/integration/schema/macros/maybe_spec.rb +32 -0
  55. data/spec/integration/schema/macros/required_spec.rb +59 -0
  56. data/spec/integration/schema/macros/when_spec.rb +65 -0
  57. data/spec/integration/schema/nested_values_spec.rb +41 -0
  58. data/spec/integration/schema/not_spec.rb +14 -14
  59. data/spec/integration/schema/option_with_default_spec.rb +30 -0
  60. data/spec/integration/schema/reusing_schema_spec.rb +33 -0
  61. data/spec/integration/schema/using_types_spec.rb +29 -0
  62. data/spec/integration/schema/xor_spec.rb +17 -14
  63. data/spec/integration/schema_spec.rb +75 -245
  64. data/spec/shared/rule_compiler.rb +8 -0
  65. data/spec/spec_helper.rb +13 -0
  66. data/spec/unit/hint_compiler_spec.rb +10 -10
  67. data/spec/unit/{input_type_compiler_spec.rb → input_processor_compiler/form_spec.rb} +88 -73
  68. data/spec/unit/schema/key_spec.rb +33 -0
  69. data/spec/unit/schema/rule_spec.rb +7 -6
  70. data/spec/unit/schema/value_spec.rb +187 -54
  71. metadata +53 -31
  72. data/.rubocop.yml +0 -16
  73. data/.rubocop_todo.yml +0 -7
  74. data/lib/dry/validation/input_type_compiler.rb +0 -83
  75. data/lib/dry/validation/schema/definition.rb +0 -74
  76. data/lib/dry/validation/schema/result.rb +0 -68
  77. data/rakelib/rubocop.rake +0 -18
  78. data/spec/integration/rule_groups_spec.rb +0 -94
  79. data/spec/integration/schema/attrs_spec.rb +0 -38
  80. data/spec/integration/schema/default_key_behavior_spec.rb +0 -23
  81. data/spec/integration/schema/grouped_rules_spec.rb +0 -57
  82. data/spec/integration/schema/nested_spec.rb +0 -31
  83. data/spec/integration/schema_form_spec.rb +0 -97
@@ -0,0 +1,122 @@
1
+ RSpec.describe Dry::Validation::Schema::Form, 'defining a schema' do
2
+ subject(:schema) do
3
+ Dry::Validation.Form do
4
+ key(:email).required
5
+
6
+ key(:age).maybe(:int?, gt?: 18)
7
+
8
+ key(:address).schema do
9
+ key(:city).required
10
+ key(:street).required
11
+
12
+ key(:loc) do
13
+ hash? do
14
+ key(:lat).required(:float?)
15
+ key(:lng).required(:float?)
16
+ end
17
+ end
18
+ end
19
+
20
+ optional(:password).maybe.confirmation
21
+
22
+ optional(:phone_number).maybe(:int?, gt?: 0)
23
+
24
+ rule(:email_valid) { value(:email).email? }
25
+
26
+ configure do
27
+ def email?(value)
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '#messages' do
35
+ it 'returns compiled error messages' do
36
+ result = schema.('email' => '', 'age' => '19')
37
+
38
+ expect(result.messages).to eql(
39
+ email: ['must be filled'],
40
+ address: ['is missing']
41
+ )
42
+
43
+ expect(result.output).to eql(email: '', age: 19)
44
+ end
45
+
46
+ it 'returns hints for nested data' do
47
+ result = schema.(
48
+ 'email' => 'jane@doe.org',
49
+ 'age' => '19',
50
+ 'address' => {
51
+ 'city' => '',
52
+ 'street' => 'Street 1/2',
53
+ 'loc' => { 'lat' => '123.456', 'lng' => '' }
54
+ }
55
+ )
56
+
57
+ expect(result.messages).to eql(
58
+ address: {
59
+ loc: { lng: ['must be filled'] },
60
+ city: ['must be filled']
61
+ }
62
+ )
63
+ end
64
+ end
65
+
66
+ describe '#call' do
67
+ it 'passes when attributes are valid' do
68
+ result = schema.(
69
+ 'email' => 'jane@doe.org',
70
+ 'age' => '19',
71
+ 'address' => {
72
+ 'city' => 'NYC',
73
+ 'street' => 'Street 1/2',
74
+ 'loc' => { 'lat' => '123.456', 'lng' => '456.123' }
75
+ }
76
+ )
77
+
78
+ expect(result).to be_success
79
+
80
+ expect(result.output).to eql(
81
+ email: 'jane@doe.org', age: 19,
82
+ address: {
83
+ city: 'NYC', street: 'Street 1/2',
84
+ loc: { lat: 123.456, lng: 456.123 }
85
+ }
86
+ )
87
+ end
88
+
89
+ it 'validates presence of an email and min age value' do
90
+ result = schema.('email' => '', 'age' => '18')
91
+
92
+ expect(result.messages).to eql(
93
+ address: ['is missing'],
94
+ age: ['must be greater than 18'],
95
+ email: ['must be filled']
96
+ )
97
+ end
98
+
99
+ it 'handles optionals' do
100
+ result = schema.(
101
+ 'email' => 'jane@doe.org',
102
+ 'age' => '19',
103
+ 'phone_number' => '12',
104
+ 'address' => {
105
+ 'city' => 'NYC',
106
+ 'street' => 'Street 1/2',
107
+ 'loc' => { 'lat' => '123.456', 'lng' => '456.123' }
108
+ }
109
+ )
110
+
111
+ expect(result).to be_success
112
+
113
+ expect(result.output).to eql(
114
+ email: 'jane@doe.org', age: 19, phone_number: 12,
115
+ address: {
116
+ city: 'NYC', street: 'Street 1/2',
117
+ loc: { lat: 123.456, lng: 456.123 }
118
+ }
119
+ )
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,31 @@
1
+ RSpec.describe 'Inheriting schema' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema(base_schema) do
4
+ key(:location).schema do
5
+ key(:lat).required(:float?)
6
+ key(:lng).required(:float?)
7
+ end
8
+ end
9
+ end
10
+
11
+ let(:base_schema) do
12
+ Dry::Validation.Schema do
13
+ key(:city).required
14
+ end
15
+ end
16
+
17
+ it 'inherits rules from parent schema' do
18
+ expect(schema.(city: 'NYC', location: { lat: 1.23, lng: 45.6 })).to be_success
19
+
20
+ expect(schema.(city: '', location: { lat: 1.23, lng: 45.6 }).messages).to eql(
21
+ city: ['must be filled']
22
+ )
23
+
24
+ expect(schema.(city: 'NYC', location: { lat: nil, lng: '45.6' }).messages).to eql(
25
+ location: {
26
+ lat: ['must be filled'],
27
+ lng: ['must be a float']
28
+ }
29
+ )
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ RSpec.describe Dry::Validation::Schema, 'setting input processor in schema' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ configure do
5
+ config.input_processor = :sanitizer
6
+ end
7
+
8
+ key(:email).required
9
+
10
+ key(:age).maybe(:int?, gt?: 18)
11
+
12
+ key(:address).schema do
13
+ key(:city).required
14
+ key(:street).required
15
+ end
16
+
17
+ key(:phone_numbers).each do
18
+ key(:prefix).required
19
+ key(:value).required
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'rejects unspecified keys' do
25
+ result = schema.(
26
+ email: 'jane@doe',
27
+ age: 19,
28
+ such: 'key',
29
+ address: { city: 'NYC', street: 'Street', wow: 'bad' },
30
+ phone_numbers: [
31
+ { prefix: '48', value: '123' },
32
+ { lol: '!!', prefix: '1', value: '312' }
33
+ ]
34
+ )
35
+
36
+ expect(result.output).to eql(
37
+ email: 'jane@doe',
38
+ age: 19,
39
+ address: { city: 'NYC', street: 'Street' },
40
+ phone_numbers: [
41
+ { prefix: '48', value: '123' },
42
+ { prefix: '1', value: '312' }
43
+ ]
44
+ )
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ RSpec.describe 'Macros #confirmation' do
2
+ describe 'with a maybe password with min-size specified' do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ configure do
6
+ def self.messages
7
+ Messages.default.merge(
8
+ en: { errors: { password_confirmation: 'does not match' } }
9
+ )
10
+ end
11
+ end
12
+
13
+ key(:password).maybe(min_size?: 3).confirmation
14
+ end
15
+ end
16
+
17
+ it 'passes when values are equal' do
18
+ expect(schema.(password: 'foo', password_confirmation: 'foo')).to be_success
19
+ end
20
+
21
+ it 'fails when source value is invalid' do
22
+ expect(schema.(password: 'fo', password_confirmation: '').messages).to eql(
23
+ password: ['size cannot be less than 3']
24
+ )
25
+ end
26
+
27
+ it 'fails when values are not equal' do
28
+ expect(schema.(password: 'foo', password_confirmation: 'fo').messages).to eql(
29
+ password_confirmation: ['does not match']
30
+ )
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ RSpec.describe 'Macros #maybe' do
2
+ describe 'with no args' do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ key(:email).maybe
6
+ end
7
+ end
8
+
9
+ it 'generates none? | filled? rule' do
10
+ expect(schema.(email: nil).messages).to be_empty
11
+ expect(schema.(email: 'jane@doe').messages).to be_empty
12
+ end
13
+ end
14
+
15
+ describe 'with a predicate with args' do
16
+ subject(:schema) do
17
+ Dry::Validation.Schema do
18
+ key(:name).maybe(min_size?: 3)
19
+ end
20
+ end
21
+
22
+ it 'generates none? | (filled? & min_size?) rule' do
23
+ expect(schema.(name: nil).messages).to be_empty
24
+
25
+ expect(schema.(name: 'jane').messages).to be_empty
26
+
27
+ expect(schema.(name: 'xy').messages).to eql(
28
+ name: ['size cannot be less than 3']
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,59 @@
1
+ RSpec.describe 'Macros #required' do
2
+ describe 'with no args' do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ key(:email).required
6
+ end
7
+ end
8
+
9
+ it 'generates filled? rule' do
10
+ expect(schema.(email: '').messages).to eql(
11
+ email: ['must be filled']
12
+ )
13
+ end
14
+ end
15
+
16
+ describe 'with a type specification' do
17
+ subject(:schema) do
18
+ Dry::Validation.Schema do
19
+ key(:age).required(:int?)
20
+ end
21
+ end
22
+
23
+ it 'generates filled? & int? rule' do
24
+ expect(schema.(age: nil).messages).to eql(
25
+ age: ['must be filled']
26
+ )
27
+ end
28
+ end
29
+
30
+ describe 'with a predicate with args' do
31
+ context 'with a flat arg' do
32
+ subject(:schema) do
33
+ Dry::Validation.Schema do
34
+ key(:age).required(:int?, gt?: 18)
35
+ end
36
+ end
37
+
38
+ it 'generates filled? & int? & gt? rule' do
39
+ expect(schema.(age: nil).messages).to eql(
40
+ age: ['must be filled', 'must be greater than 18']
41
+ )
42
+ end
43
+ end
44
+
45
+ context 'with a range arg' do
46
+ subject(:schema) do
47
+ Dry::Validation.Schema do
48
+ key(:age).required(:int?, size?: 18..24)
49
+ end
50
+ end
51
+
52
+ it 'generates filled? & int? & gt? rule' do
53
+ expect(schema.(age: nil).messages).to eql(
54
+ age: ['must be filled', 'size must be within 18 - 24']
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,65 @@
1
+ RSpec.describe 'Macros #when' do
2
+ context 'with a result rule returned from the block' do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ key(:email).maybe
6
+
7
+ key(:login).required.when(:true?) do
8
+ value(:email).filled?
9
+ end
10
+ end
11
+ end
12
+
13
+ it 'generates check rule' do
14
+ expect(schema.(login: true, email: nil).messages).to eql(
15
+ email: ['must be filled']
16
+ )
17
+
18
+ expect(schema.(login: false, email: nil).messages).to be_empty
19
+ end
20
+ end
21
+
22
+ describe 'with a result rule depending on another result' do
23
+ subject(:schema) do
24
+ Dry::Validation.Schema do
25
+ key(:left).maybe(:int?)
26
+ key(:right).maybe(:int?)
27
+
28
+ key(:compare).maybe(:bool?).when(:true?) do
29
+ value(:left).gt?(value(:right))
30
+ end
31
+ end
32
+ end
33
+
34
+ it 'generates check rule' do
35
+ expect(schema.(compare: false, left: nil, right: nil)).to be_success
36
+
37
+ expect(schema.(compare: true, left: 1, right: 2).messages).to eql(
38
+ left: ['must be greater than 2']
39
+ )
40
+ end
41
+ end
42
+
43
+ describe 'with multiple result rules' do
44
+ subject(:schema) do
45
+ Dry::Validation.Schema do
46
+ key(:email).maybe
47
+ key(:password).maybe
48
+
49
+ key(:login).maybe(:bool?).when(:true?) do
50
+ value(:email).filled?
51
+ value(:password).filled?
52
+ end
53
+ end
54
+ end
55
+
56
+ it 'generates check rule' do
57
+ expect(schema.(login: false, email: nil, password: nil)).to be_success
58
+
59
+ expect(schema.(login: true, email: nil, password: nil).messages).to eql(
60
+ email: ['must be filled'],
61
+ password: ['must be filled']
62
+ )
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.describe Schema, 'using nested values' do
2
+ let(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:email).maybe
5
+
6
+ key(:settings) do
7
+ optional(:offers).required(:bool?).when(:true?) do
8
+ value([:settings, :newsletter]).false?
9
+ end
10
+
11
+ key(:newsletter).required(:bool?).when(:true?) do
12
+ value(:email).filled?
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ it 'passes when newsletter setting is false' do
19
+ expect(schema.(settings: { newsletter: false }, email: nil)).to be_success
20
+ end
21
+
22
+ it 'passes when newsletter setting is true and email is filled' do
23
+ expect(schema.(settings: { newsletter: false }, email: 'jane@doe')).to be_success
24
+ end
25
+
26
+ it 'passes when offers is false and newsletter is true' do
27
+ expect(schema.(settings: { offers: false, newsletter: true }, email: 'jane@doe')).to be_success
28
+ end
29
+
30
+ it 'fails when newsletter is true and email is not filled' do
31
+ expect(schema.(settings: { newsletter: true }, email: nil).messages).to eql(
32
+ email: ['must be filled']
33
+ )
34
+ end
35
+
36
+ it 'fails when offers is true and newsletter is true' do
37
+ expect(schema.(settings: { offers: true, newsletter: true }, email: 'jane@doe').messages).to eql(
38
+ settings: { newsletter: ['must be false'] }
39
+ )
40
+ end
41
+ end
@@ -1,34 +1,34 @@
1
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
- )
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ configure do
5
+ def self.messages
6
+ Messages.default.merge(
7
+ en: { errors: { be_reasonable: 'you cannot eat cake and have cake!' } }
8
+ )
9
+ end
10
10
  end
11
11
 
12
- optional(:eat_cake) { |v| v.eql?('yes!') }
13
- optional(:have_cake) { |v| v.eql?('yes!') }
12
+ optional(:eat_cake).required
13
+ optional(:have_cake).required
14
14
 
15
15
  rule(:be_reasonable) do
16
- rule(:eat_cake) & rule(:have_cake).not
16
+ value(:eat_cake).eql?('yes!') & value(:have_cake).eql?('yes!').not
17
17
  end
18
18
  end
19
19
  end
20
20
 
21
21
  describe '#messages' do
22
22
  it 'passes when only one option is selected' do
23
- messages = validate.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
23
+ messages = schema.(eat_cake: 'yes!', have_cake: 'no!').messages[:be_reasonable]
24
24
 
25
25
  expect(messages).to be(nil)
26
26
  end
27
27
 
28
28
  it 'fails when both options are selected' do
29
- messages = validate.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
29
+ messages = schema.(eat_cake: 'yes!', have_cake: 'yes!').messages[:be_reasonable]
30
30
 
31
- expect(messages).to eql([['you cannot eat cake and have cake!'], 'yes!'])
31
+ expect(messages).to eql(['you cannot eat cake and have cake!'])
32
32
  end
33
33
  end
34
34
  end