dry-validation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.rubocop_todo.yml +7 -0
  6. data/.travis.yml +29 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE +20 -0
  10. data/README.md +297 -0
  11. data/Rakefile +12 -0
  12. data/config/errors.yml +35 -0
  13. data/dry-validation.gemspec +25 -0
  14. data/examples/basic.rb +21 -0
  15. data/examples/nested.rb +30 -0
  16. data/examples/rule_ast.rb +33 -0
  17. data/lib/dry-validation.rb +1 -0
  18. data/lib/dry/validation.rb +12 -0
  19. data/lib/dry/validation/error.rb +43 -0
  20. data/lib/dry/validation/error_compiler.rb +116 -0
  21. data/lib/dry/validation/messages.rb +71 -0
  22. data/lib/dry/validation/predicate.rb +39 -0
  23. data/lib/dry/validation/predicate_set.rb +22 -0
  24. data/lib/dry/validation/predicates.rb +88 -0
  25. data/lib/dry/validation/result.rb +64 -0
  26. data/lib/dry/validation/rule.rb +125 -0
  27. data/lib/dry/validation/rule_compiler.rb +57 -0
  28. data/lib/dry/validation/schema.rb +74 -0
  29. data/lib/dry/validation/schema/definition.rb +15 -0
  30. data/lib/dry/validation/schema/key.rb +39 -0
  31. data/lib/dry/validation/schema/rule.rb +28 -0
  32. data/lib/dry/validation/schema/value.rb +31 -0
  33. data/lib/dry/validation/version.rb +5 -0
  34. data/rakelib/rubocop.rake +18 -0
  35. data/spec/fixtures/errors.yml +4 -0
  36. data/spec/integration/custom_error_messages_spec.rb +35 -0
  37. data/spec/integration/custom_predicates_spec.rb +57 -0
  38. data/spec/integration/validation_spec.rb +118 -0
  39. data/spec/shared/predicates.rb +31 -0
  40. data/spec/spec_helper.rb +18 -0
  41. data/spec/unit/error_compiler_spec.rb +165 -0
  42. data/spec/unit/predicate_spec.rb +37 -0
  43. data/spec/unit/predicates/empty_spec.rb +38 -0
  44. data/spec/unit/predicates/eql_spec.rb +21 -0
  45. data/spec/unit/predicates/exclusion_spec.rb +35 -0
  46. data/spec/unit/predicates/filled_spec.rb +38 -0
  47. data/spec/unit/predicates/format_spec.rb +21 -0
  48. data/spec/unit/predicates/gt_spec.rb +40 -0
  49. data/spec/unit/predicates/gteq_spec.rb +40 -0
  50. data/spec/unit/predicates/inclusion_spec.rb +35 -0
  51. data/spec/unit/predicates/int_spec.rb +34 -0
  52. data/spec/unit/predicates/key_spec.rb +29 -0
  53. data/spec/unit/predicates/lt_spec.rb +40 -0
  54. data/spec/unit/predicates/lteq_spec.rb +40 -0
  55. data/spec/unit/predicates/max_size_spec.rb +49 -0
  56. data/spec/unit/predicates/min_size_spec.rb +49 -0
  57. data/spec/unit/predicates/nil_spec.rb +28 -0
  58. data/spec/unit/predicates/size_spec.rb +49 -0
  59. data/spec/unit/predicates/str_spec.rb +32 -0
  60. data/spec/unit/rule/each_spec.rb +20 -0
  61. data/spec/unit/rule/key_spec.rb +27 -0
  62. data/spec/unit/rule/set_spec.rb +32 -0
  63. data/spec/unit/rule/value_spec.rb +42 -0
  64. data/spec/unit/rule_compiler_spec.rb +86 -0
  65. metadata +230 -0
@@ -0,0 +1,18 @@
1
+ begin
2
+ require 'rubocop/rake_task'
3
+
4
+ Rake::Task[:default].enhance [:rubocop]
5
+
6
+ RuboCop::RakeTask.new do |task|
7
+ task.options << '--display-cop-names'
8
+ end
9
+
10
+ namespace :rubocop do
11
+ desc 'Generate a configuration file acting as a TODO list.'
12
+ task :auto_gen_config do
13
+ exec 'bundle exec rubocop --auto-gen-config'
14
+ end
15
+ end
16
+
17
+ rescue LoadError
18
+ end
@@ -0,0 +1,4 @@
1
+ user:
2
+ attributes:
3
+ email:
4
+ filled?: "%{name} can't be blank"
@@ -0,0 +1,35 @@
1
+ RSpec.describe Dry::Validation, 'with custom messages' do
2
+ subject(:validation) { schema.new }
3
+
4
+ describe 'defining schema' do
5
+ let(:schema) do
6
+ Class.new(Dry::Validation::Schema) do
7
+ configure do |config|
8
+ config.messages_file = SPEC_ROOT.join('fixtures/errors.yml')
9
+ config.namespace = :user
10
+ end
11
+
12
+ key(:email) { |email| email.filled? }
13
+ end
14
+ end
15
+
16
+ let(:attrs) do
17
+ {
18
+ email: 'jane@doe.org',
19
+ age: 19,
20
+ address: { city: 'NYC', street: 'Street 1/2', country: { code: 'US', name: 'USA' } },
21
+ phone_numbers: [
22
+ '123456', '234567'
23
+ ]
24
+ }.freeze
25
+ end
26
+
27
+ describe '#messages' do
28
+ it 'returns compiled error messages' do
29
+ expect(validation.messages(attrs.merge(email: ''))).to eql([
30
+ [:email, ["email can't be blank"]]
31
+ ])
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,57 @@
1
+ RSpec.describe Dry::Validation do
2
+ subject(:validation) { schema.new }
3
+
4
+ shared_context 'uses custom predicates' do
5
+ it 'uses provided custom predicates' do
6
+ expect(validation.(email: 'jane@doe')).to be_empty
7
+
8
+ expect(validation.(email: nil)).to match_array([
9
+ [:error, [:input, [:email, nil, [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
10
+ ])
11
+
12
+ expect(validation.(email: 'jane')).to match_array([
13
+ [:error, [:input, [:email, 'jane', [[:val, [:email, [:predicate, [:email?, []]]]]]]]]
14
+ ])
15
+ end
16
+ end
17
+
18
+ describe 'defining schema with custom predicates container' do
19
+ let(:schema) do
20
+ Class.new(Dry::Validation::Schema) do
21
+ configure do |config|
22
+ config.predicates = Test::Predicates
23
+ end
24
+
25
+ key(:email) { |value| value.filled? & value.email? }
26
+ end
27
+ end
28
+
29
+ before do
30
+ module Test
31
+ module Predicates
32
+ include Dry::Validation::Predicates
33
+
34
+ predicate(:email?) do |input|
35
+ input.include?('@') # for the lols
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ include_context 'uses custom predicates'
42
+ end
43
+
44
+ describe 'defining schema with custom predicate methods' do
45
+ let(:schema) do
46
+ Class.new(Dry::Validation::Schema) do
47
+ key(:email) { |value| value.filled? & value.email? }
48
+
49
+ def email?(value)
50
+ value.include?('@')
51
+ end
52
+ end
53
+ end
54
+
55
+ include_context 'uses custom predicates'
56
+ end
57
+ end
@@ -0,0 +1,118 @@
1
+ RSpec.describe Dry::Validation do
2
+ subject(:validation) { schema.new }
3
+
4
+ describe 'defining schema' do
5
+ let(:schema) do
6
+ Class.new(Dry::Validation::Schema) do
7
+ key(:email) { |email| email.filled? }
8
+
9
+ key(:age) do |age|
10
+ age.int? & age.gt?(18)
11
+ end
12
+
13
+ key(:address) do |address|
14
+ address.key(:city) do |city|
15
+ city.min_size?(3)
16
+ end
17
+
18
+ address.key(:street) do |street|
19
+ street.filled?
20
+ end
21
+
22
+ address.key(:country) do |country|
23
+ country.key(:name, &:filled?)
24
+ country.key(:code, &:filled?)
25
+ end
26
+ end
27
+
28
+ key(:phone_numbers) do |phone_numbers|
29
+ phone_numbers.each(&:str?)
30
+ end
31
+ end
32
+ end
33
+
34
+ let(:attrs) do
35
+ {
36
+ email: 'jane@doe.org',
37
+ age: 19,
38
+ address: { city: 'NYC', street: 'Street 1/2', country: { code: 'US', name: 'USA' } },
39
+ phone_numbers: [
40
+ '123456', '234567'
41
+ ]
42
+ }.freeze
43
+ end
44
+
45
+ describe '#messages' do
46
+ it 'returns compiled error messages' do
47
+ expect(validation.messages(attrs.merge(email: ''))).to eql([
48
+ [:email, ["email must be filled"]]
49
+ ])
50
+ end
51
+ end
52
+
53
+ describe '#call' do
54
+ it 'passes when attributes are valid' do
55
+ expect(validation.(attrs)).to be_empty
56
+ end
57
+
58
+ it 'validates presence of an email and min age value' do
59
+ expect(validation.(attrs.merge(email: '', age: 18))).to match_array([
60
+ [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
61
+ [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]]
62
+ ])
63
+ end
64
+
65
+ it 'validates presence of the email key and type of age value' do
66
+ expect(validation.(name: 'Jane', age: '18', address: attrs[:address], phone_numbers: attrs[:phone_numbers])).to match_array([
67
+ [:error, [:input, [:age, "18", [[:val, [:age, [:predicate, [:int?, []]]]]]]]],
68
+ [:error, [:input, [:email, nil, [[:key, [:email, [:predicate, [:key?, [:email]]]]]]]]]
69
+ ])
70
+ end
71
+
72
+ it 'validates presence of the address and phone_number keys' do
73
+ expect(validation.(email: 'jane@doe.org', age: 19)).to match_array([
74
+ [:error, [:input, [:address, nil, [[:key, [:address, [:predicate, [:key?, [:address]]]]]]]]],
75
+ [:error, [:input, [:phone_numbers, nil, [[:key, [:phone_numbers, [:predicate, [:key?, [:phone_numbers]]]]]]]]]
76
+ ])
77
+ end
78
+
79
+ it 'validates presence of keys under address and min size of the city value' do
80
+ expect(validation.(attrs.merge(address: { city: 'NY' }))).to match_array([
81
+ [:error, [
82
+ :input, [
83
+ :address, {city: "NY"},
84
+ [
85
+ [:input, [:city, "NY", [[:val, [:city, [:predicate, [:min_size?, [3]]]]]]]],
86
+ [:input, [:street, nil, [[:key, [:street, [:predicate, [:key?, [:street]]]]]]]],
87
+ [:input, [:country, nil, [[:key, [:country, [:predicate, [:key?, [:country]]]]]]]]
88
+ ]
89
+ ]
90
+ ]]
91
+ ])
92
+ end
93
+
94
+ it 'validates address code and name values' do
95
+ expect(validation.(attrs.merge(address: attrs[:address].merge(country: { code: 'US', name: '' })))).to match_array([
96
+ [:error, [
97
+ :input, [
98
+ :address, {city: "NYC", street: "Street 1/2", country: {code: "US", name: ""}},
99
+ [
100
+ [
101
+ :input, [
102
+ :country, {code: "US", name: ""}, [
103
+ [
104
+ :input, [
105
+ :name, "", [[:val, [:name, [:predicate, [:filled?, []]]]]]
106
+ ]
107
+ ]
108
+ ]
109
+ ]
110
+ ]
111
+ ]
112
+ ]
113
+ ]]
114
+ ])
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,31 @@
1
+ require 'dry/validation/predicates'
2
+
3
+ RSpec.shared_examples 'predicates' do
4
+ let(:nil?) { Dry::Validation::Predicates[:nil?] }
5
+
6
+ let(:str?) { Dry::Validation::Predicates[:str?] }
7
+
8
+ let(:min_size?) { Dry::Validation::Predicates[:min_size?] }
9
+
10
+ let(:key?) { Dry::Validation::Predicates[:key?] }
11
+ end
12
+
13
+ RSpec.shared_examples 'a passing predicate' do
14
+ let(:predicate) { Dry::Validation::Predicates[predicate_name] }
15
+
16
+ it do
17
+ arguments_list.each do |args|
18
+ expect(predicate.call(*args)).to be true
19
+ end
20
+ end
21
+ end
22
+
23
+ RSpec.shared_examples 'a failing predicate' do
24
+ let(:predicate) { Dry::Validation::Predicates[predicate_name] }
25
+
26
+ it do
27
+ arguments_list.each do |args|
28
+ expect(predicate.call(*args)).to be false
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ require 'dry-validation'
4
+
5
+ begin
6
+ require 'byebug'
7
+ rescue LoadError; end
8
+
9
+ SPEC_ROOT = Pathname(__dir__)
10
+
11
+ Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:require))
12
+ Dir[SPEC_ROOT.join('support/**/*.rb')].each(&method(:require))
13
+
14
+ include Dry::Validation
15
+
16
+ RSpec.configure do |config|
17
+ config.disable_monkey_patching!
18
+ end
@@ -0,0 +1,165 @@
1
+ require 'dry/validation/messages'
2
+ require 'dry/validation/error_compiler'
3
+
4
+ RSpec.describe Dry::Validation::ErrorCompiler do
5
+ subject(:error_compiler) { ErrorCompiler.new(messages) }
6
+
7
+ let(:messages) do
8
+ Messages.default.merge(
9
+ key?: '+%{name}+ key is missing in the hash',
10
+ attributes: {
11
+ address: {
12
+ filled?: 'Please provide your address'
13
+ }
14
+ }
15
+ )
16
+ end
17
+
18
+ describe '#call' do
19
+ let(:ast) do
20
+ [
21
+ [:error, [:input, [:name, nil, [[:key, [:name, [:predicate, [:key?, []]]]]]]]],
22
+ [:error, [:input, [:age, 18, [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]],
23
+ [:error, [:input, [:email, "", [[:val, [:email, [:predicate, [:filled?, []]]]]]]]],
24
+ [:error, [:input, [:address, "", [[:val, [:address, [:predicate, [:filled?, []]]]]]]]]
25
+ ]
26
+ end
27
+
28
+ it 'converts error ast into another format' do
29
+ expect(error_compiler.(ast)).to eql([
30
+ [:name, ["+name+ key is missing in the hash"]],
31
+ [:age, ["age must be greater than 18 (18 was given)"]],
32
+ [:email, ["email must be filled"]],
33
+ [:address, ["Please provide your address"]]
34
+ ])
35
+ end
36
+ end
37
+
38
+ describe '#visit_predicate' do
39
+ describe ':empty?' do
40
+ it 'returns valid message' do
41
+ msg = error_compiler.visit_predicate([:empty?, []], [], :tags)
42
+
43
+ expect(msg).to eql('tags cannot be empty')
44
+ end
45
+ end
46
+
47
+ describe ':exclusion?' do
48
+ it 'returns valid message' do
49
+ msg = error_compiler.visit_predicate([:exclusion?, [[1, 2, 3]]], 2, :num)
50
+
51
+ expect(msg).to eql('num must not be one of: 1, 2, 3')
52
+ end
53
+ end
54
+
55
+ describe ':inclusion?' do
56
+ it 'returns valid message' do
57
+ msg = error_compiler.visit_predicate([:inclusion?, [[1, 2, 3]]], 2, :num)
58
+
59
+ expect(msg).to eql('num must be one of: 1, 2, 3')
60
+ end
61
+ end
62
+
63
+ describe ':gt?' do
64
+ it 'returns valid message' do
65
+ msg = error_compiler.visit_predicate([:gt?, [3]], 2, :num)
66
+
67
+ expect(msg).to eql('num must be greater than 3 (2 was given)')
68
+ end
69
+ end
70
+
71
+ describe ':gteq?' do
72
+ it 'returns valid message' do
73
+ msg = error_compiler.visit_predicate([:gteq?, [3]], 2, :num)
74
+
75
+ expect(msg).to eql('num must be greater than or equal to 3')
76
+ end
77
+ end
78
+
79
+ describe ':lt?' do
80
+ it 'returns valid message' do
81
+ msg = error_compiler.visit_predicate([:lt?, [3]], 2, :num)
82
+
83
+ expect(msg).to eql('num must be less than 3 (2 was given)')
84
+ end
85
+ end
86
+
87
+ describe ':lteq?' do
88
+ it 'returns valid message' do
89
+ msg = error_compiler.visit_predicate([:lteq?, [3]], 2, :num)
90
+
91
+ expect(msg).to eql('num must be less than or equal to 3')
92
+ end
93
+ end
94
+
95
+ describe ':int?' do
96
+ it 'returns valid message' do
97
+ msg = error_compiler.visit_predicate([:int?, []], '2', :num)
98
+
99
+ expect(msg).to eql('num must be an integer')
100
+ end
101
+ end
102
+
103
+ describe ':max_size?' do
104
+ it 'returns valid message' do
105
+ msg = error_compiler.visit_predicate([:max_size?, [3]], 'abcd', :num)
106
+
107
+ expect(msg).to eql('num size cannot be greater than 3')
108
+ end
109
+ end
110
+
111
+ describe ':min_size?' do
112
+ it 'returns valid message' do
113
+ msg = error_compiler.visit_predicate([:min_size?, [3]], 'ab', :num)
114
+
115
+ expect(msg).to eql('num size cannot be less than 3')
116
+ end
117
+ end
118
+
119
+ describe ':nil?' do
120
+ it 'returns valid message' do
121
+ msg = error_compiler.visit_predicate([:nil?, []], nil, :num)
122
+
123
+ expect(msg).to eql('num cannot be nil')
124
+ end
125
+ end
126
+
127
+ describe ':size?' do
128
+ it 'returns valid message when arg is int' do
129
+ msg = error_compiler.visit_predicate([:size?, [3]], 'ab', :num)
130
+
131
+ expect(msg).to eql('num size must be 3')
132
+ end
133
+
134
+ it 'returns valid message when arg is range' do
135
+ msg = error_compiler.visit_predicate([:size?, [3..4]], 'ab', :num)
136
+
137
+ expect(msg).to eql('num size must be within 3 - 4')
138
+ end
139
+ end
140
+
141
+ describe ':str?' do
142
+ it 'returns valid message' do
143
+ msg = error_compiler.visit_predicate([:str?, []], 3, :num)
144
+
145
+ expect(msg).to eql('num must be a string')
146
+ end
147
+ end
148
+
149
+ describe ':format?' do
150
+ it 'returns valid message' do
151
+ msg = error_compiler.visit_predicate([:format?, [/^F/]], 'Bar', :str)
152
+
153
+ expect(msg).to eql('str is in invalid format')
154
+ end
155
+ end
156
+
157
+ describe ':eql?' do
158
+ it 'returns valid message' do
159
+ msg = error_compiler.visit_predicate([:eql?, ['Bar']], 'Foo', :str)
160
+
161
+ expect(msg).to eql('str must be equal to Bar')
162
+ end
163
+ end
164
+ end
165
+ end