dry-validation 0.1.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 (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