dry-validation 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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