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
@@ -1,19 +1,23 @@
1
1
  require 'dry/validation/messages/i18n'
2
2
 
3
3
  RSpec.describe 'Validation hints' do
4
- subject(:validation) { schema.new }
5
-
6
4
  shared_context '#messages' do
7
5
  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']
6
+ expect(schema.(age: '17').messages).to eql(
7
+ age: ['must be an integer', 'must be greater than 18']
8
+ )
9
+ end
10
+
11
+ it 'skips type-check rules' do
12
+ expect(schema.(age: 17).messages).to eql(
13
+ age: ['must be greater than 18']
10
14
  )
11
15
  end
12
16
  end
13
17
 
14
18
  context 'with yaml messages' do
15
- let(:schema) do
16
- Class.new(Dry::Validation::Schema) do
19
+ subject(:schema) do
20
+ Dry::Validation.Schema do
17
21
  key(:age) do |age|
18
22
  age.none? | (age.int? & age.gt?(18))
19
23
  end
@@ -24,9 +28,9 @@ RSpec.describe 'Validation hints' do
24
28
  end
25
29
 
26
30
  context 'with i18n messages' do
27
- let(:schema) do
28
- Class.new(Dry::Validation::Schema) do
29
- configure { |c| c.messages = :i18n }
31
+ subject(:schema) do
32
+ Dry::Validation.Schema do
33
+ configure { configure { |c| c.messages = :i18n } }
30
34
 
31
35
  key(:age) do |age|
32
36
  age.none? | (age.int? & age.gt?(18))
@@ -36,4 +40,19 @@ RSpec.describe 'Validation hints' do
36
40
 
37
41
  include_context '#messages'
38
42
  end
43
+
44
+ context 'when type expectation is specified' do
45
+ subject(:schema) do
46
+ Dry::Validation.Schema do
47
+ key(:email).required
48
+ key(:name).required(:str?, size?: 5..25)
49
+ end
50
+ end
51
+
52
+ it 'infers message for specific type' do
53
+ expect(schema.(email: 'jane@doe', name: 'HN').messages).to eql(
54
+ name: ['length must be within 5 - 25']
55
+ )
56
+ end
57
+ end
39
58
  end
@@ -1,23 +1,22 @@
1
1
  RSpec.describe 'Schema / Injecting Rules' do
2
- let(:validate) { schema.new(other.rules) }
2
+ subject(:schema) do
3
+ Dry::Validation.Schema(rules: other.class.rules) do
4
+ key(:email).maybe
3
5
 
4
- let(:other) do
5
- Class.new(Dry::Validation::Schema) do
6
- key(:login) { |value| value.bool? }
6
+ rule(:email) { value(:login).true? > value(:email).filled? }
7
7
  end
8
8
  end
9
9
 
10
- let(:schema) do
11
- Class.new(Dry::Validation::Schema) do
12
- key(:email) { |email| email.none? | email.filled? }
13
-
14
- rule(email: :filled?) { value(:login).true? > value(:email).filled? }
10
+ let(:other) do
11
+ Dry::Validation.Schema do
12
+ key(:login) { |value| value.bool? }
15
13
  end
16
14
  end
17
15
 
18
16
  it 'appends rules from another schema' do
19
- expect(validate.(login: true, email: 'jane@doe')).to be_empty
20
- expect(validate.(login: false, email: nil)).to be_empty
21
- expect(validate.(login: true, email: nil)).to_not be_empty
17
+ expect(schema.(login: true, email: 'jane@doe')).to be_success
18
+ expect(schema.(login: false, email: nil)).to be_success
19
+ expect(schema.(login: true, email: nil)).to_not be_success
20
+ expect(schema.(login: nil, email: 'jane@doe')).to_not be_success
22
21
  end
23
22
  end
@@ -1,8 +1,6 @@
1
1
  require 'dry/validation/messages/i18n'
2
2
 
3
3
  RSpec.describe Dry::Validation, 'with localized messages' do
4
- subject(:validation) { schema.new }
5
-
6
4
  before do
7
5
  I18n.config.available_locales_set << :pl
8
6
  I18n.load_path.concat(%w(en pl).map { |l| SPEC_ROOT.join("fixtures/locales/#{l}.yml") })
@@ -11,9 +9,9 @@ RSpec.describe Dry::Validation, 'with localized messages' do
11
9
 
12
10
  describe 'defining schema' do
13
11
  context 'without a namespace' do
14
- let(:schema) do
15
- Class.new(Dry::Validation::Schema) do
16
- configure do |config|
12
+ subject(:schema) do
13
+ Dry::Validation.Schema do
14
+ configure do
17
15
  config.messages = :i18n
18
16
  end
19
17
 
@@ -23,19 +21,21 @@ RSpec.describe Dry::Validation, 'with localized messages' do
23
21
 
24
22
  describe '#messages' do
25
23
  it 'returns localized error messages' do
26
- expect(validation.(email: '').messages(locale: :pl)).to match_array([
27
- [:email, [['Proszę podać adres email'], '']]
28
- ])
24
+ expect(schema.(email: '').messages(locale: :pl)).to eql(
25
+ email: ['Proszę podać adres email']
26
+ )
29
27
  end
30
28
  end
31
29
  end
32
30
 
33
31
  context 'with a namespace' do
34
- let(:schema) do
35
- Class.new(Dry::Validation::Schema) do
36
- configure do |config|
37
- config.messages = :i18n
38
- config.namespace = :user
32
+ subject(:schema) do
33
+ Dry::Validation.Schema do
34
+ configure do
35
+ configure do |config|
36
+ config.messages = :i18n
37
+ config.namespace = :user
38
+ end
39
39
  end
40
40
 
41
41
  key(:email) { |email| email.filled? }
@@ -44,9 +44,9 @@ RSpec.describe Dry::Validation, 'with localized messages' do
44
44
 
45
45
  describe '#messages' do
46
46
  it 'returns localized error messages' do
47
- expect(validation.(email: '').messages(locale: :pl)).to match_array([
48
- [:email, [['Hej user! Dawaj ten email no!'], '']]
49
- ])
47
+ expect(schema.(email: '').messages(locale: :pl)).to eql(
48
+ email: ['Hej user! Dawaj ten email no!']
49
+ )
50
50
  end
51
51
  end
52
52
  end
@@ -11,10 +11,14 @@ RSpec.describe Messages::I18n do
11
11
 
12
12
  describe '#[]' do
13
13
  context 'with the default locale' do
14
+ it 'returns nil when message is not defined' do
15
+ expect(messages[:not_here, rule: :srsly]).to be(nil)
16
+ end
17
+
14
18
  it 'returns a message for a predicate' do
15
19
  message = messages[:filled?, rule: :name]
16
20
 
17
- expect(message).to eql("%{name} must be filled")
21
+ expect(message).to eql("must be filled")
18
22
  end
19
23
 
20
24
  it 'returns a message for a specific rule' do
@@ -26,19 +30,19 @@ RSpec.describe Messages::I18n do
26
30
  it 'returns a message for a specific val type' do
27
31
  message = messages[:size?, rule: :pages, val_type: String]
28
32
 
29
- expect(message).to eql("%{name} length must be %{num}")
33
+ expect(message).to eql("length must be %{num}")
30
34
  end
31
35
 
32
36
  it 'returns a message for a specific rule and its default arg type' do
33
37
  message = messages[:size?, rule: :pages]
34
38
 
35
- expect(message).to eql("%{name} size must be %{num}")
39
+ expect(message).to eql("size must be %{num}")
36
40
  end
37
41
 
38
42
  it 'returns a message for a specific rule and its arg type' do
39
43
  message = messages[:size?, rule: :pages, arg_type: Range]
40
44
 
41
- expect(message).to eql("%{name} size must be within %{left} - %{right}")
45
+ expect(message).to eql("size must be within %{left} - %{right}")
42
46
  end
43
47
  end
44
48
 
@@ -46,7 +50,7 @@ RSpec.describe Messages::I18n do
46
50
  it 'returns a message for a predicate' do
47
51
  message = messages[:filled?, rule: :name, locale: :pl]
48
52
 
49
- expect(message).to eql("%{name} nie może być pusty")
53
+ expect(message).to eql("nie może być pusty")
50
54
  end
51
55
 
52
56
  it 'returns a message for a specific rule' do
@@ -1,17 +1,15 @@
1
1
  RSpec.describe Dry::Validation::Schema do
2
- subject(:validation) { schema.new }
3
-
4
2
  describe 'defining schema with optional keys' do
5
- let(:schema) do
6
- Class.new(Dry::Validation::Schema) do
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
7
5
  optional(:email) { |email| email.filled? }
8
6
 
9
- key(:address) do |address|
10
- address.key(:city, &:filled?)
11
- address.key(:street, &:filled?)
7
+ key(:address) do
8
+ key(:city, &:filled?)
9
+ key(:street, &:filled?)
12
10
 
13
- address.optional(:phone_number) do |phone_number|
14
- phone_number.none? | phone_number.str?
11
+ optional(:phone_number) do
12
+ none? | str?
15
13
  end
16
14
  end
17
15
  end
@@ -19,11 +17,11 @@ RSpec.describe Dry::Validation::Schema do
19
17
 
20
18
  describe '#call' do
21
19
  it 'skips rules when key is not present' do
22
- expect(validation.(address: { city: 'NYC', street: 'Street 1/2' })).to be_empty
20
+ expect(schema.(address: { city: 'NYC', street: 'Street 1/2' })).to be_success
23
21
  end
24
22
 
25
23
  it 'applies rules when key is present' do
26
- expect(validation.(email: '')).to_not be_empty
24
+ expect(schema.(email: '')).to_not be_success
27
25
  end
28
26
  end
29
27
  end
@@ -0,0 +1,23 @@
1
+ RSpec.describe Dry::Validation::Schema, 'for an array' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ each do
5
+ key(:prefix).required
6
+ key(:value).required
7
+ end
8
+ end
9
+ end
10
+
11
+ it 'applies its rules to array input' do
12
+ result = schema.([{ prefix: 1, value: 123 }, { prefix: 2, value: 456 }])
13
+
14
+ expect(result).to be_success
15
+
16
+ result = schema.([{ prefix: 1, value: nil }, { prefix: nil, value: 456 }])
17
+
18
+ expect(result.messages).to eql(
19
+ 0 => { value: ["must be filled"] },
20
+ 1 => { prefix: ["must be filled"] }
21
+ )
22
+ end
23
+ end
@@ -1,49 +1,57 @@
1
1
  RSpec.describe Schema, 'using high-level rules' do
2
- subject(:validate) { schema.new }
3
-
4
2
  context 'composing rules' do
5
- let(:schema) do
6
- Class.new(Schema) do
7
- def self.messages
8
- Messages.default.merge(
9
- en: { errors: { destiny: 'you must select either red or blue' } }
10
- )
3
+ subject(:schema) do
4
+ Dry::Validation.Schema do
5
+ configure do
6
+ def self.messages
7
+ Messages.default.merge(
8
+ en: { errors: { destiny: 'you must select either red or blue' } }
9
+ )
10
+ end
11
11
  end
12
12
 
13
- optional(:red, &:filled?)
14
- optional(:blue, &:filled?)
13
+ optional(:red).maybe
14
+ optional(:blue).maybe
15
15
 
16
- rule(:destiny) { rule(:red) | rule(:blue) }
16
+ rule(destiny: [:red, :blue]) do |red, blue|
17
+ red.filled? | blue.filled?
18
+ end
17
19
  end
18
20
  end
19
21
 
20
22
  it 'passes when only red is filled' do
21
- expect(validate.(red: '1')).to be_empty
23
+ expect(schema.(red: '1')).to be_success
24
+ end
25
+
26
+ it 'fails when keys are missing' do
27
+ expect(schema.({})).to be_failure
22
28
  end
23
29
 
24
30
  it 'fails when red and blue are not filled ' do
25
- expect(validate.(red: '', blue: '').messages[:destiny]).to eql(
26
- [['you must select either red or blue'], '']
31
+ expect(schema.(red: nil, blue: nil).messages[:destiny]).to eql(
32
+ ['you must select either red or blue']
27
33
  )
28
34
  end
29
35
  end
30
36
 
31
37
  context 'composing specific predicates' do
32
38
  let(:schema) do
33
- Class.new(Schema) do
34
- def self.messages
35
- Messages.default.merge(
36
- en: {
37
- errors: {
38
- email_presence: 'email must be present when login is set to true',
39
- email_absence: 'email must not be present when login is set to false'
39
+ Dry::Validation.Schema do
40
+ configure do
41
+ def self.messages
42
+ Messages.default.merge(
43
+ en: {
44
+ errors: {
45
+ email_presence: 'must be present when login is set to true',
46
+ email_absence: 'must not be present when login is set to false'
47
+ }
40
48
  }
41
- }
42
- )
49
+ )
50
+ end
43
51
  end
44
52
 
45
- key(:login) { |login| login.bool? }
46
- key(:email) { |email| email.none? | email.filled? }
53
+ key(:login).required(:bool?)
54
+ key(:email).maybe
47
55
 
48
56
  rule(:email_presence) { value(:login).true?.then(value(:email).filled?) }
49
57
 
@@ -52,22 +60,22 @@ RSpec.describe Schema, 'using high-level rules' do
52
60
  end
53
61
 
54
62
  it 'passes when login is false and email is nil' do
55
- expect(validate.(login: false, email: nil)).to be_empty
63
+ expect(schema.(login: false, email: nil)).to be_success
56
64
  end
57
65
 
58
66
  it 'fails when login is false and email is present' do
59
- expect(validate.(login: false, email: 'jane@doe').messages[:email_absence]).to eql(
60
- [['email must not be present when login is set to false'], nil]
67
+ expect(schema.(login: false, email: 'jane@doe').messages).to eql(
68
+ email_absence: ['must not be present when login is set to false']
61
69
  )
62
70
  end
63
71
 
64
72
  it 'passes when login is true and email is present' do
65
- expect(validate.(login: true, email: 'jane@doe')).to be_empty
73
+ expect(schema.(login: true, email: 'jane@doe')).to be_success
66
74
  end
67
75
 
68
76
  it 'fails when login is true and email is not present' do
69
- expect(validate.(login: true, email: nil).messages[:email_presence]).to eql(
70
- [['email must be present when login is set to true'], nil]
77
+ expect(schema.(login: true, email: nil).messages).to eql(
78
+ email_presence: ['must be present when login is set to true']
71
79
  )
72
80
  end
73
81
  end
@@ -0,0 +1,25 @@
1
+ RSpec.describe 'Check depending on nth element in an array' do
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:tags).each(:str?)
5
+
6
+ rule(red: [[:tags, 0]]) do |value|
7
+ value.eql?('red')
8
+ end
9
+ end
10
+ end
11
+
12
+ it 'skips check when dependency failed' do
13
+ expect(schema.(tags: 'oops')).to be_failure
14
+ end
15
+
16
+ it 'passes when check passes' do
17
+ expect(schema.(tags: %w(red green blue))).to be_success
18
+ end
19
+
20
+ it 'fails when check fails' do
21
+ expect(schema.(tags: %w(blue green red)).messages).to eql(
22
+ tags: { 0 => ["must be equal to red"] }
23
+ )
24
+ end
25
+ end
@@ -1,20 +1,22 @@
1
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
2
+ subject(:schema) do
3
+ Dry::Validation.Schema do
4
+ key(:payments).each do
5
+ key(:method).required(:str?)
6
+ key(:amount).required(:float?)
13
7
  end
14
8
  end
15
9
  end
16
10
 
17
11
  describe '#messages' do
12
+ it 'validates using all rules' do
13
+ expect(schema.(payments: [{}]).messages).to eql(
14
+ { payments: {
15
+ 0 => { method: ['is missing'], amount: ['is missing'] }
16
+ }}
17
+ )
18
+ end
19
+
18
20
  it 'validates each payment against its set of rules' do
19
21
  input = {
20
22
  payments: [
@@ -23,7 +25,7 @@ RSpec.describe 'Schema with each and set rules' do
23
25
  ]
24
26
  }
25
27
 
26
- expect(validation.(input).messages).to eql({})
28
+ expect(schema.(input).messages).to eql({})
27
29
  end
28
30
 
29
31
  it 'validates presence of the method key for each payment' do
@@ -34,10 +36,9 @@ RSpec.describe 'Schema with each and set rules' do
34
36
  ]
35
37
  }
36
38
 
37
- expect(validation.(input).messages[:payments]).to eql([
38
- [payments: [[method: [["method is missing"], nil]], input[:payments][1]]],
39
- input[:payments]
40
- ])
39
+ expect(schema.(input).messages).to eql(
40
+ payments: { 1 => { method: ['is missing'] } }
41
+ )
41
42
  end
42
43
 
43
44
  it 'validates type of the method value for each payment' do
@@ -48,10 +49,9 @@ RSpec.describe 'Schema with each and set rules' do
48
49
  ]
49
50
  }
50
51
 
51
- expect(validation.(input).messages[:payments]).to eql([
52
- [payments: [[method: [["method must be a string"], 12]], input[:payments][1]]],
53
- input[:payments]
54
- ])
52
+ expect(schema.(input).messages).to eql(
53
+ payments: { 1 => { method: ['must be a string'] } }
54
+ )
55
55
  end
56
56
 
57
57
  it 'validates type of the amount value for each payment' do
@@ -62,10 +62,9 @@ RSpec.describe 'Schema with each and set rules' do
62
62
  ]
63
63
  }
64
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
- ])
65
+ expect(schema.(input).messages).to eql(
66
+ payments: { 1 => { amount: ['must be a float'] } }
67
+ )
69
68
  end
70
69
  end
71
70
  end