reform 2.3.0.rc1 → 2.3.0.rc2

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/.gitignore +2 -0
  3. data/.rubocop.yml +30 -0
  4. data/.rubocop_todo.yml +460 -0
  5. data/.travis.yml +26 -11
  6. data/CHANGES.md +25 -2
  7. data/Gemfile +6 -3
  8. data/ISSUE_TEMPLATE.md +1 -1
  9. data/README.md +2 -4
  10. data/Rakefile +18 -9
  11. data/lib/reform/contract.rb +7 -7
  12. data/lib/reform/contract/custom_error.rb +41 -0
  13. data/lib/reform/contract/validate.rb +9 -5
  14. data/lib/reform/errors.rb +27 -15
  15. data/lib/reform/form.rb +22 -11
  16. data/lib/reform/form/call.rb +1 -1
  17. data/lib/reform/form/composition.rb +2 -2
  18. data/lib/reform/form/dry.rb +10 -86
  19. data/lib/reform/form/dry/input_hash.rb +37 -0
  20. data/lib/reform/form/dry/new_api.rb +58 -0
  21. data/lib/reform/form/dry/old_api.rb +61 -0
  22. data/lib/reform/form/populator.rb +9 -11
  23. data/lib/reform/form/prepopulate.rb +3 -2
  24. data/lib/reform/form/validate.rb +19 -12
  25. data/lib/reform/result.rb +36 -9
  26. data/lib/reform/validation.rb +10 -8
  27. data/lib/reform/validation/groups.rb +2 -3
  28. data/lib/reform/version.rb +1 -1
  29. data/reform.gemspec +10 -9
  30. data/test/benchmarking.rb +10 -11
  31. data/test/call_new_api.rb +23 -0
  32. data/test/{call_test.rb → call_old_api.rb} +3 -3
  33. data/test/changed_test.rb +7 -7
  34. data/test/coercion_test.rb +50 -18
  35. data/test/composition_new_api.rb +186 -0
  36. data/test/{composition_test.rb → composition_old_api.rb} +23 -26
  37. data/test/contract/custom_error_test.rb +55 -0
  38. data/test/contract_new_api.rb +77 -0
  39. data/test/{contract_test.rb → contract_old_api.rb} +8 -8
  40. data/test/default_test.rb +1 -1
  41. data/test/deserialize_test.rb +8 -11
  42. data/test/errors_new_api.rb +225 -0
  43. data/test/errors_old_api.rb +230 -0
  44. data/test/feature_test.rb +7 -9
  45. data/test/fixtures/dry_error_messages.yml +5 -2
  46. data/test/fixtures/dry_new_api_error_messages.yml +104 -0
  47. data/test/form_new_api.rb +57 -0
  48. data/test/{form_test.rb → form_old_api.rb} +2 -2
  49. data/test/form_option_new_api.rb +24 -0
  50. data/test/{form_option_test.rb → form_option_old_api.rb} +1 -1
  51. data/test/from_test.rb +8 -12
  52. data/test/inherit_new_api.rb +105 -0
  53. data/test/{inherit_test.rb → inherit_old_api.rb} +10 -17
  54. data/test/module_new_api.rb +137 -0
  55. data/test/{module_test.rb → module_old_api.rb} +19 -15
  56. data/test/parse_option_test.rb +5 -5
  57. data/test/parse_pipeline_test.rb +2 -2
  58. data/test/populate_new_api.rb +304 -0
  59. data/test/{populate_test.rb → populate_old_api.rb} +28 -34
  60. data/test/populator_skip_test.rb +1 -2
  61. data/test/prepopulator_test.rb +5 -6
  62. data/test/read_only_test.rb +12 -1
  63. data/test/readable_test.rb +5 -5
  64. data/test/reform_new_api.rb +204 -0
  65. data/test/{reform_test.rb → reform_old_api.rb} +17 -23
  66. data/test/save_new_api.rb +101 -0
  67. data/test/{save_test.rb → save_old_api.rb} +10 -13
  68. data/test/setup_test.rb +6 -6
  69. data/test/{skip_if_test.rb → skip_if_new_api.rb} +20 -9
  70. data/test/skip_if_old_api.rb +92 -0
  71. data/test/skip_setter_and_getter_test.rb +2 -3
  72. data/test/test_helper.rb +13 -5
  73. data/test/validate_new_api.rb +408 -0
  74. data/test/{validate_test.rb → validate_old_api.rb} +43 -53
  75. data/test/validation/dry_validation_new_api.rb +826 -0
  76. data/test/validation/{dry_validation_test.rb → dry_validation_old_api.rb} +223 -116
  77. data/test/validation/result_test.rb +20 -22
  78. data/test/validation_library_provided_test.rb +3 -3
  79. data/test/virtual_test.rb +46 -6
  80. data/test/writeable_test.rb +7 -7
  81. metadata +101 -51
  82. data/test/errors_test.rb +0 -180
  83. data/test/readonly_test.rb +0 -14
@@ -1,4 +1,4 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
3
  class ContractValidateTest < MiniTest::Spec
4
4
  Song = Struct.new(:title, :album, :composer)
@@ -30,13 +30,13 @@ class ContractValidateTest < MiniTest::Spec
30
30
  end
31
31
  end
32
32
 
33
- let (:song) { Song.new("Broken") }
34
- let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
35
- let (:composer) { Artist.new("Greg Graffin") }
36
- let (:artist) { Artist.new("Bad Religion") }
37
- let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
33
+ let(:song) { Song.new("Broken") }
34
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
35
+ let(:composer) { Artist.new("Greg Graffin") }
36
+ let(:artist) { Artist.new("Bad Religion") }
37
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
38
38
 
39
- let (:form) { AlbumForm.new(album) }
39
+ let(:form) { AlbumForm.new(album) }
40
40
 
41
41
  # valid
42
42
  it do
@@ -54,7 +54,6 @@ class ContractValidateTest < MiniTest::Spec
54
54
  end
55
55
  end
56
56
 
57
-
58
57
  # no configuration results in "sync" (formerly known as parse_strategy: :sync).
59
58
  class ValidateWithoutConfigurationTest < MiniTest::Spec
60
59
  Song = Struct.new(:title, :album, :composer)
@@ -68,7 +67,6 @@ class ValidateWithoutConfigurationTest < MiniTest::Spec
68
67
  end
69
68
 
70
69
  collection :songs do
71
-
72
70
  property :title
73
71
  validation do
74
72
  required(:title).filled
@@ -87,23 +85,25 @@ class ValidateWithoutConfigurationTest < MiniTest::Spec
87
85
  end
88
86
  end
89
87
 
90
- let (:song) { Song.new("Broken") }
91
- let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
92
- let (:composer) { Artist.new("Greg Graffin") }
93
- let (:artist) { Artist.new("Bad Religion") }
94
- let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
88
+ let(:song) { Song.new("Broken") }
89
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
90
+ let(:composer) { Artist.new("Greg Graffin") }
91
+ let(:artist) { Artist.new("Bad Religion") }
92
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
95
93
 
96
- let (:form) { AlbumForm.new(album) }
94
+ let(:form) { AlbumForm.new(album) }
97
95
 
98
96
  # valid.
99
97
  it do
100
- object_ids = {song: form.songs[0].object_id, song_with_composer: form.songs[1].object_id,
101
- artist: form.artist.object_id, composer: form.songs[1].composer.object_id}
98
+ object_ids = {
99
+ song: form.songs[0].object_id, song_with_composer: form.songs[1].object_id,
100
+ artist: form.artist.object_id, composer: form.songs[1].composer.object_id
101
+ }
102
102
 
103
103
  form.validate(
104
104
  "name" => "Best Of",
105
105
  "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne", "composer" => {"name" => "Sting"}}],
106
- "artist" => {"name" => "The Police"},
106
+ "artist" => {"name" => "The Police"}
107
107
  ).must_equal true
108
108
 
109
109
  form.errors.messages.inspect.must_equal "{}"
@@ -121,7 +121,6 @@ class ValidateWithoutConfigurationTest < MiniTest::Spec
121
121
  form.songs[1].composer.object_id.must_equal object_ids[:composer]
122
122
  form.artist.object_id.must_equal object_ids[:artist]
123
123
 
124
-
125
124
  # model has not changed, yet.
126
125
  album.name.must_equal "The Dissent Of Man"
127
126
  album.songs[0].title.must_equal "Broken"
@@ -135,7 +134,7 @@ class ValidateWithoutConfigurationTest < MiniTest::Spec
135
134
  form.validate(
136
135
  name: "Best Of",
137
136
  songs: [{title: "The X-Creep"}, {title: "Trudging", composer: {name: "SNFU"}}],
138
- artist: {name: "The Police"},
137
+ artist: {name: "The Police"}
139
138
  ).must_equal true
140
139
 
141
140
  form.name.must_equal "Best Of"
@@ -167,16 +166,16 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
167
166
  end
168
167
 
169
168
  collection :songs,
170
- internal_populator: lambda { |input, options|
171
- collection = options[:represented].songs
172
- (item = collection[options[:index]]) ? item : collection.insert(options[:index], Song.new) } do
173
-
169
+ internal_populator: ->(input, options) {
170
+ collection = options[:represented].songs
171
+ (item = collection[options[:index]]) ? item : collection.insert(options[:index], Song.new)
172
+ } do
174
173
  property :title
175
174
  validation do
176
175
  required(:title).filled
177
176
  end
178
177
 
179
- property :composer, internal_populator: lambda { |input, options| (item = options[:represented].composer) ? item : Artist.new } do
178
+ property :composer, internal_populator: ->(input, options) { (item = options[:represented].composer) ? item : Artist.new } do
180
179
  property :name
181
180
  validation do
182
181
  required(:name).filled
@@ -184,7 +183,7 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
184
183
  end
185
184
  end
186
185
 
187
- property :artist, internal_populator: lambda { |input, options| (item = options[:represented].artist) ? item : Artist.new } do
186
+ property :artist, internal_populator: ->(input, options) { (item = options[:represented].artist) ? item : Artist.new } do
188
187
  property :name
189
188
  validation do
190
189
  required(:name).filled
@@ -192,13 +191,13 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
192
191
  end
193
192
  end
194
193
 
195
- let (:song) { Song.new("Broken") }
196
- let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
197
- let (:composer) { Artist.new("Greg Graffin") }
198
- let (:artist) { Artist.new("Bad Religion") }
199
- let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
194
+ let(:song) { Song.new("Broken") }
195
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
196
+ let(:composer) { Artist.new("Greg Graffin") }
197
+ let(:artist) { Artist.new("Bad Religion") }
198
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
200
199
 
201
- let (:form) { AlbumForm.new(album) }
200
+ let(:form) { AlbumForm.new(album) }
202
201
 
203
202
  # valid.
204
203
  it("xxx") do
@@ -207,6 +206,7 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
207
206
  "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne", "composer" => {"name" => "Sting"}}],
208
207
  "artist" => {"name" => "The Police"},
209
208
  ).must_equal true
209
+ form.valid?.must_equal true
210
210
 
211
211
  form.errors.messages.inspect.must_equal "{}"
212
212
 
@@ -217,7 +217,6 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
217
217
  form.songs[1].composer.name.must_equal "Sting"
218
218
  form.artist.name.must_equal "The Police"
219
219
 
220
-
221
220
  # model has not changed, yet.
222
221
  album.name.must_equal "The Dissent Of Man"
223
222
  album.songs[0].title.must_equal "Broken"
@@ -233,6 +232,7 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
233
232
  "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne", "composer" => {"name" => ""}}],
234
233
  "artist" => {"name" => ""},
235
234
  ).must_equal false
235
+ form.valid?.must_equal false
236
236
 
237
237
  form.errors.messages.inspect.must_equal "{:name=>[\"must be filled\"], :\"songs.composer.name\"=>[\"must be filled\"], :\"artist.name\"=>[\"must be filled\"]}"
238
238
  end
@@ -256,7 +256,6 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
256
256
  form.songs.size.must_equal 3
257
257
  form.artist.name.must_equal "Bad Religion"
258
258
 
259
-
260
259
  # model has not changed, yet.
261
260
  album.name.must_equal "The Dissent Of Man"
262
261
  album.songs[0].title.must_equal "Broken"
@@ -266,10 +265,9 @@ class ValidateWithInternalPopulatorOptionTest < MiniTest::Spec
266
265
  album.artist.name.must_equal "Bad Religion"
267
266
  end
268
267
 
269
-
270
268
  # allow writeable: false even in the deserializer.
271
269
  class SongForm < TestForm
272
- property :title, deserializer: { writeable: false }
270
+ property :title, deserializer: {writeable: false}
273
271
  end
274
272
 
275
273
  it do
@@ -284,7 +282,7 @@ end
284
282
 
285
283
  # # not sure if we should catch that in Reform or rather do that in disposable. this is https://github.com/trailblazer/reform/pull/104
286
284
  # # describe ":populator with :empty" do
287
- # # let (:form) {
285
+ # # let(:form) {
288
286
  # # Class.new(Reform::Form) do
289
287
  # # collection :songs, :empty => true, :populator => lambda { |fragment, index, args|
290
288
  # # songs[index] = args.binding[:form].new(Song.new)
@@ -294,7 +292,7 @@ end
294
292
  # # end
295
293
  # # }
296
294
 
297
- # # let (:params) {
295
+ # # let(:params) {
298
296
  # # {
299
297
  # # "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"}]
300
298
  # # }
@@ -308,10 +306,9 @@ end
308
306
  # # it { subject.songs[1].title.must_equal "Roxanne" }
309
307
  # # end
310
308
 
311
-
312
309
  # # test cardinalities.
313
310
  # describe "with empty collection and cardinality" do
314
- # let (:album) { Album.new }
311
+ # let(:album) { Album.new }
315
312
 
316
313
  # subject { Class.new(Reform::Form) do
317
314
  # include Reform::Form::ActiveModel
@@ -329,7 +326,6 @@ end
329
326
  # validates :hit, :presence => true
330
327
  # end.new(album) }
331
328
 
332
-
333
329
  # describe "invalid" do
334
330
  # before { subject.validate({}).must_equal false }
335
331
 
@@ -342,9 +338,8 @@ end
342
338
  # end
343
339
  # end
344
340
 
345
-
346
341
  # describe "valid" do
347
- # let (:album) { Album.new(nil, Song.new, [Song.new("Urban Myth")]) }
342
+ # let(:album) { Album.new(nil, Song.new, [Song.new("Urban Myth")]) }
348
343
 
349
344
  # before {
350
345
  # subject.validate({"songs" => [{"title"=>"Daddy, Brother, Lover, Little Boy"}], "hit" => {"title"=>"The Horse"}}).
@@ -355,13 +350,9 @@ end
355
350
  # end
356
351
  # end
357
352
 
358
-
359
-
360
-
361
-
362
353
  # # providing manual validator method allows accessing form's API.
363
354
  # describe "with ::validate" do
364
- # let (:form) {
355
+ # let(:form) {
365
356
  # Class.new(Reform::Form) do
366
357
  # property :title
367
358
 
@@ -373,8 +364,8 @@ end
373
364
  # end
374
365
  # }
375
366
 
376
- # let (:params) { {"title" => "Fallout"} }
377
- # let (:song) { Song.new("Englishman") }
367
+ # let(:params) { {"title" => "Fallout"} }
368
+ # let(:song) { Song.new("Englishman") }
378
369
 
379
370
  # subject { form.new(song) }
380
371
 
@@ -384,10 +375,9 @@ end
384
375
  # it { subject.errors.messages.must_equal({:title=>["not lowercase"]}) }
385
376
  # end
386
377
 
387
-
388
378
  # # overriding the reader for a nested form should only be considered when rendering.
389
379
  # describe "with overridden reader for nested form" do
390
- # let (:form) {
380
+ # let(:form) {
391
381
  # Class.new(Reform::Form) do
392
382
  # property :band, :populate_if_empty => lambda { |*| Band.new } do
393
383
  # property :label
@@ -407,7 +397,7 @@ end
407
397
  # end.new(album)
408
398
  # }
409
399
 
410
- # let (:album) { Album.new }
400
+ # let(:album) { Album.new }
411
401
 
412
402
  # # don't use #artist when validating!
413
403
  # it do
@@ -0,0 +1,826 @@
1
+ require "test_helper"
2
+ require "reform/form/dry"
3
+ require "reform/form/coercion"
4
+ #---
5
+ # one "nested" Schema per form.
6
+ class DryValidationErrorsAPITest < Minitest::Spec
7
+ Album = Struct.new(:title, :artist, :songs)
8
+ Song = Struct.new(:title)
9
+ Artist = Struct.new(:email, :label)
10
+ Label = Struct.new(:location)
11
+
12
+ class AlbumForm < TestForm
13
+ property :title
14
+
15
+ validation do
16
+ params do
17
+ required(:title).filled(min_size?: 2)
18
+ end
19
+ end
20
+
21
+ property :artist do
22
+ property :email
23
+
24
+ validation do
25
+ params { required(:email).filled }
26
+ end
27
+
28
+ property :label do
29
+ property :location
30
+
31
+ validation do
32
+ params { required(:location).filled }
33
+ end
34
+ end
35
+ end
36
+
37
+ # note the validation block is *in* the collection block, per item, so to speak.
38
+ collection :songs do
39
+ property :title
40
+
41
+ validation do
42
+ config.messages.load_paths << "test/fixtures/dry_new_api_error_messages.yml"
43
+
44
+ params { required(:title).filled }
45
+ end
46
+ end
47
+ end
48
+
49
+ let(:form) { AlbumForm.new(Album.new(nil, Artist.new(nil, Label.new), [Song.new(nil), Song.new(nil)])) }
50
+
51
+ it "everything wrong" do
52
+ result = form.(title: nil, artist: {email: ""}, songs: [{title: "Clams have feelings too"}, {title: ""}])
53
+
54
+ result.success?.must_equal false
55
+
56
+ form.errors.messages.must_equal(title: ["must be filled", "size cannot be less than 2"], "artist.email": ["must be filled"], "artist.label.location": ["must be filled"], "songs.title": ["must be filled"])
57
+ form.artist.errors.messages.must_equal(email: ["must be filled"], "label.location": ["must be filled"])
58
+ form.artist.label.errors.messages.must_equal(location: ["must be filled"])
59
+ form.songs[0].errors.messages.must_equal({})
60
+ form.songs[1].errors.messages.must_equal(title: ["must be filled"])
61
+
62
+ # #errors[]
63
+ form.errors[:nonsense].must_equal []
64
+ form.errors[:title].must_equal ["must be filled", "size cannot be less than 2"]
65
+ form.artist.errors[:email].must_equal ["must be filled"]
66
+ form.artist.label.errors[:location].must_equal ["must be filled"]
67
+ form.songs[0].errors[:title].must_equal []
68
+ form.songs[1].errors[:title].must_equal ["must be filled"]
69
+
70
+ # #to_result
71
+ form.to_result.errors.must_equal(title: ["must be filled"])
72
+ form.to_result.messages.must_equal(title: ["must be filled", "size cannot be less than 2"])
73
+ form.to_result.hints.must_equal(title: ["size cannot be less than 2"])
74
+ form.artist.to_result.errors.must_equal(email: ["must be filled"])
75
+ form.artist.to_result.messages.must_equal(email: ["must be filled"])
76
+ form.artist.to_result.hints.must_equal(email: [])
77
+ form.artist.label.to_result.errors.must_equal(location: ["must be filled"])
78
+ form.artist.label.to_result.messages.must_equal(location: ["must be filled"])
79
+ form.artist.label.to_result.hints.must_equal(location: [])
80
+ form.songs[0].to_result.errors.must_equal({})
81
+ form.songs[0].to_result.messages.must_equal({})
82
+ form.songs[0].to_result.hints.must_equal({})
83
+ form.songs[1].to_result.errors.must_equal(title: ["must be filled"])
84
+ form.songs[1].to_result.messages.must_equal(title: ["must be filled"])
85
+ form.songs[1].to_result.hints.must_equal(title: [])
86
+ form.songs[1].to_result.errors(locale: :de).must_equal(title: ["muss abgefüllt sein"])
87
+ # seems like dry-v when calling Dry::Schema::Result#messages locale option is ignored
88
+ # started a topic in their forum https://discourse.dry-rb.org/t/dry-result-messages-ignore-locale-option/910
89
+ # form.songs[1].to_result.messages(locale: :de).must_equal(title: ["muss abgefüllt sein"])
90
+ form.songs[1].to_result.hints(locale: :de).must_equal(title: [])
91
+ end
92
+
93
+ it "only nested property is invalid." do
94
+ result = form.(title: "Black Star", artist: {email: ""})
95
+
96
+ result.success?.must_equal false
97
+
98
+ # errors.messages
99
+ form.errors.messages.must_equal("artist.email": ["must be filled"], "artist.label.location": ["must be filled"], "songs.title": ["must be filled"])
100
+ form.artist.errors.messages.must_equal(email: ["must be filled"], "label.location": ["must be filled"])
101
+ form.artist.label.errors.messages.must_equal(location: ["must be filled"])
102
+ end
103
+
104
+ it "nested collection invalid" do
105
+ result = form.(title: "Black Star", artist: {email: "uhm", label: {location: "Hannover"}}, songs: [{title: ""}])
106
+
107
+ result.success?.must_equal false
108
+ form.errors.messages.must_equal("songs.title": ["must be filled"])
109
+ end
110
+
111
+ #---
112
+ #- validation .each
113
+ class CollectionExternalValidationsForm < TestForm
114
+ collection :songs do
115
+ property :title
116
+ end
117
+
118
+ validation do
119
+ params do
120
+ required(:songs).each do
121
+ schema do
122
+ required(:title).filled
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ it do
130
+ form = CollectionExternalValidationsForm.new(Album.new(nil, nil, [Song.new, Song.new]))
131
+ form.validate(songs: [{title: "Liar"}, {title: ""}])
132
+
133
+ form.errors.messages.must_equal("songs.title": ["must be filled"])
134
+ form.songs[0].errors.messages.must_equal({})
135
+ form.songs[1].errors.messages.must_equal(title: ["must be filled"])
136
+ end
137
+ end
138
+
139
+ class DryValidationExplicitSchemaTest < Minitest::Spec
140
+ Session = Struct.new(:name, :email)
141
+ SessionSchema = Dry::Validation.Contract do
142
+ params do
143
+ required(:name).filled
144
+ required(:email).filled
145
+ end
146
+ end
147
+
148
+ class SessionForm < TestForm
149
+ include Coercion
150
+
151
+ property :name
152
+ property :email
153
+
154
+ validation schema: SessionSchema
155
+ end
156
+
157
+ let(:form) { SessionForm.new(Session.new) }
158
+
159
+ # valid.
160
+ it do
161
+ form.validate(name: "Helloween", email: "yep").must_equal true
162
+ form.errors.messages.inspect.must_equal "{}"
163
+ end
164
+
165
+ it "invalid" do
166
+ form.validate(name: "", email: "yep").must_equal false
167
+ form.errors.messages.inspect.must_equal "{:name=>[\"must be filled\"]}"
168
+ end
169
+ end
170
+
171
+ class DryValidationDefaultGroupTest < Minitest::Spec
172
+ Session = Struct.new(:username, :email, :password, :confirm_password, :starts_at, :active, :color)
173
+
174
+ class SessionForm < TestForm
175
+ include Coercion
176
+
177
+ property :username
178
+ property :email
179
+ property :password
180
+ property :confirm_password
181
+ property :starts_at, type: DRY_TYPES_CONSTANT::DateTime
182
+ property :active, type: DRY_TYPES_CONSTANT::Bool
183
+ property :color
184
+
185
+ validation do
186
+ params do
187
+ required(:username).filled
188
+ required(:email).filled
189
+ required(:starts_at).filled(:date_time?)
190
+ required(:active).filled(:bool?)
191
+ end
192
+ end
193
+
194
+ validation name: :another_block do
195
+ params { required(:confirm_password).filled }
196
+ end
197
+
198
+ validation name: :dynamic_args, with: {form: true} do
199
+ params { required(:color).maybe(included_in?: form.colors) }
200
+ end
201
+
202
+ def colors
203
+ %(red orange green)
204
+ end
205
+ end
206
+
207
+ let(:form) { SessionForm.new(Session.new) }
208
+
209
+ # valid.
210
+ it do
211
+ form.validate(
212
+ username: "Helloween",
213
+ email: "yep",
214
+ starts_at: "01/01/2000 - 11:00",
215
+ active: "true",
216
+ confirm_password: "pA55w0rd"
217
+ ).must_equal true
218
+ form.active.must_equal true
219
+ form.errors.messages.inspect.must_equal "{}"
220
+ end
221
+
222
+ it "invalid" do
223
+ form.validate(
224
+ username: "Helloween",
225
+ email: "yep",
226
+ active: "1",
227
+ starts_at: "01/01/2000 - 11:00",
228
+ color: "purple"
229
+ ).must_equal false
230
+ form.active.must_equal true
231
+ form.errors.messages.inspect.must_equal "{:confirm_password=>[\"must be filled\"], :color=>[\"must be one of: red orange green\"]}"
232
+ end
233
+ end
234
+
235
+ class ValidationGroupsTest < MiniTest::Spec
236
+ describe "basic validations" do
237
+ Session = Struct.new(:username, :email, :password, :confirm_password, :special_class)
238
+ SomeClass = Struct.new(:id)
239
+
240
+ class SessionForm < TestForm
241
+ property :username
242
+ property :email
243
+ property :password
244
+ property :confirm_password
245
+ property :special_class
246
+
247
+ validation do
248
+ params do
249
+ required(:username).filled
250
+ required(:email).filled
251
+ required(:special_class).filled(type?: SomeClass)
252
+ end
253
+ end
254
+
255
+ validation name: :email, if: :default do
256
+ params { required(:email).filled(min_size?: 3) }
257
+ end
258
+
259
+ validation name: :password, if: :email do
260
+ params { required(:password).filled(min_size?: 2) }
261
+ end
262
+
263
+ validation name: :confirm, if: :default, after: :email do
264
+ params { required(:confirm_password).filled(min_size?: 2) }
265
+ end
266
+ end
267
+
268
+ let(:form) { SessionForm.new(Session.new) }
269
+
270
+ # valid.
271
+ it do
272
+ form.validate(username: "Helloween",
273
+ special_class: SomeClass.new(id: 15),
274
+ email: "yep",
275
+ password: "99",
276
+ confirm_password: "99").must_equal true
277
+ form.errors.messages.inspect.must_equal "{}"
278
+ end
279
+
280
+ # invalid.
281
+ it do
282
+ form.validate({}).must_equal false
283
+ form.errors.messages.must_equal({username: ["must be filled"], email: ["must be filled"], special_class: ["must be filled", "must be ValidationGroupsTest::SomeClass"]})
284
+ end
285
+
286
+ # partially invalid.
287
+ # 2nd group fails.
288
+ it do
289
+ form.validate(username: "Helloween", email: "yo", confirm_password: "9", special_class: SomeClass.new(id: 15)).must_equal false
290
+ form.errors.messages.inspect.must_equal "{:email=>[\"size cannot be less than 3\"], :confirm_password=>[\"size cannot be less than 2\"]}"
291
+ end
292
+ # 3rd group fails.
293
+ it do
294
+ form.validate(username: "Helloween", email: "yo!", confirm_password: "9", special_class: SomeClass.new(id: 15)).must_equal false
295
+ form.errors.messages.inspect
296
+ .must_equal "{:confirm_password=>[\"size cannot be less than 2\"], :password=>[\"must be filled\", \"size cannot be less than 2\"]}"
297
+ end
298
+ # 4th group with after: fails.
299
+ it do
300
+ form.validate(username: "Helloween", email: "yo!", password: "1", confirm_password: "9", special_class: SomeClass.new(id: 15)).must_equal false
301
+ form.errors.messages.inspect.must_equal "{:confirm_password=>[\"size cannot be less than 2\"], :password=>[\"size cannot be less than 2\"]}"
302
+ end
303
+ end
304
+
305
+ class ValidationWithOptionsTest < MiniTest::Spec
306
+ describe "basic validations" do
307
+ Session = Struct.new(:username)
308
+ class SessionForm < TestForm
309
+ property :username
310
+
311
+ validation name: :default, with: {user: OpenStruct.new(name: "Nick")} do
312
+ params do
313
+ required(:username).filled(eql?: user.name)
314
+ end
315
+ end
316
+ end
317
+
318
+ let(:form) { SessionForm.new(Session.new) }
319
+
320
+ # valid.
321
+ it do
322
+ form.validate(username: "Nick").must_equal true
323
+ form.errors.messages.inspect.must_equal "{}"
324
+ end
325
+
326
+ # invalid.
327
+ it do
328
+ form.validate(username: "Fred").must_equal false
329
+ form.errors.messages.inspect.must_equal "{:username=>[\"must be equal to Nick\"]}"
330
+ end
331
+ end
332
+ end
333
+
334
+ #---
335
+ #- validation( schema: MySchema )
336
+ describe "with custom schema" do
337
+ Session2 = Struct.new(:username, :email, :password)
338
+
339
+ MySchema = Dry::Schema.Params do
340
+ config.messages.load_paths << "test/fixtures/dry_error_messages.yml"
341
+
342
+ required(:password).filled(min_size?: 6)
343
+ end
344
+
345
+ class Session2Form < TestForm
346
+ property :username
347
+ property :email
348
+ property :password
349
+
350
+ validation schema: MySchema do
351
+ params do
352
+ required(:username).filled
353
+ required(:email).filled
354
+ end
355
+
356
+ rule(:email) do
357
+ key.failure(:good_musical_taste?) unless value.is_a? String
358
+ end
359
+ end
360
+ end
361
+
362
+ let(:form) { Session2Form.new(Session2.new) }
363
+
364
+ # valid.
365
+ it do
366
+ skip "waiting dry-v to add this as feature https://github.com/dry-rb/dry-schema/issues/33"
367
+ form.validate(username: "Helloween", email: "yep", password: "extrasafe").must_equal true
368
+ form.errors.messages.inspect.must_equal "{}"
369
+ end
370
+
371
+ # invalid.
372
+ it do
373
+ skip "waiting dry-v to add this as feature https://github.com/dry-rb/dry-schema/issues/33"
374
+ form.validate({}).must_equal false
375
+ form.errors.messages.must_equal(password: ["must be filled", "size cannot be less than 6"], username: ["must be filled"], email: ["must be filled", "you're a bad person"])
376
+ end
377
+
378
+ it do
379
+ skip "waiting dry-v to add this as feature https://github.com/dry-rb/dry-schema/issues/33"
380
+ form.validate(email: 1).must_equal false
381
+ form.errors.messages.inspect.must_equal "{:password=>[\"must be filled\", \"size cannot be less than 6\"], :username=>[\"must be filled\"], :email=>[\"you're a bad person\"]}"
382
+ end
383
+ end
384
+
385
+ describe "MIXED nested validations" do
386
+ class AlbumForm < TestForm
387
+ property :title
388
+
389
+ property :hit do
390
+ property :title
391
+
392
+ validation do
393
+ params { required(:title).filled }
394
+ end
395
+ end
396
+
397
+ collection :songs do
398
+ property :title
399
+
400
+ validation do
401
+ params { required(:title).filled }
402
+ end
403
+ end
404
+
405
+ # we test this one by running an each / schema dry-v check on the main block
406
+ collection :producers do
407
+ property :name
408
+ end
409
+
410
+ property :band do
411
+ property :name
412
+ property :label do
413
+ property :location
414
+ end
415
+ end
416
+
417
+ validation do
418
+ config.messages.load_paths << "test/fixtures/dry_new_api_error_messages.yml"
419
+ params do
420
+ required(:title).filled
421
+ required(:band).hash do
422
+ required(:name).filled
423
+ required(:label).hash do
424
+ required(:location).filled
425
+ end
426
+ end
427
+
428
+ required(:producers).each do
429
+ hash { required(:name).filled }
430
+ end
431
+ end
432
+
433
+ rule(:title) do
434
+ key.failure(:good_musical_taste?) unless value != "Nickelback"
435
+ end
436
+ end
437
+ end
438
+
439
+ let(:album) do
440
+ OpenStruct.new(
441
+ hit: OpenStruct.new,
442
+ songs: [OpenStruct.new, OpenStruct.new],
443
+ band: Struct.new(:name, :label).new("", OpenStruct.new),
444
+ producers: [OpenStruct.new, OpenStruct.new, OpenStruct.new],
445
+ )
446
+ end
447
+
448
+ let(:form) { AlbumForm.new(album) }
449
+
450
+ it "maps errors to form objects correctly" do
451
+ result = form.validate(
452
+ "title" => "Nickelback",
453
+ "songs" => [{"title" => ""}, {"title" => ""}],
454
+ "band" => {"size" => "", "label" => {"location" => ""}},
455
+ "producers" => [{"name" => ""}, {"name" => "something lovely"}]
456
+ )
457
+
458
+ result.must_equal false
459
+ # from nested validation
460
+ form.errors.messages.must_equal(title: ["you're a bad person"], "hit.title": ["must be filled"], "songs.title": ["must be filled"], "producers.name": ["must be filled"], "band.name": ["must be filled"], "band.label.location": ["must be filled"])
461
+
462
+ # songs have their own validation.
463
+ form.songs[0].errors.messages.must_equal(title: ["must be filled"])
464
+ # hit got its own validation group.
465
+ form.hit.errors.messages.must_equal(title: ["must be filled"])
466
+
467
+ form.band.label.errors.messages.must_equal(location: ["must be filled"])
468
+ form.band.errors.messages.must_equal(name: ["must be filled"], "label.location": ["must be filled"])
469
+ form.producers[0].errors.messages.must_equal(name: ["must be filled"])
470
+
471
+ # TODO: use the same form structure as the top one and do the same test against messages, errors and hints.
472
+ form.producers[0].to_result.errors.must_equal(name: ["must be filled"])
473
+ form.producers[0].to_result.messages.must_equal(name: ["must be filled"])
474
+ form.producers[0].to_result.hints.must_equal(name: [])
475
+ end
476
+
477
+ # FIXME: fix the "must be filled error"
478
+
479
+ it "renders full messages correctly" do
480
+ result = form.validate(
481
+ "title" => "",
482
+ "songs" => [{"title" => ""}, {"title" => ""}],
483
+ "band" => {"size" => "", "label" => {"name" => ""}},
484
+ "producers" => [{"name" => ""}, {"name" => ""}, {"name" => "something lovely"}]
485
+ )
486
+
487
+ result.must_equal false
488
+ form.band.errors.full_messages.must_equal ["Name must be filled", "Label Location must be filled"]
489
+ form.band.label.errors.full_messages.must_equal ["Location must be filled"]
490
+ form.producers.first.errors.full_messages.must_equal ["Name must be filled"]
491
+ form.errors.full_messages.must_equal ["Title must be filled", "Hit Title must be filled", "Songs Title must be filled", "Producers Name must be filled", "Band Name must be filled", "Band Label Location must be filled"]
492
+ end
493
+
494
+ describe "only 1 nested validation" do
495
+ class AlbumFormWith1NestedVal < TestForm
496
+ property :title
497
+ property :band do
498
+ property :name
499
+ property :label do
500
+ property :location
501
+ end
502
+ end
503
+
504
+ validation do
505
+ config.messages.load_paths << "test/fixtures/dry_new_api_error_messages.yml"
506
+
507
+ params do
508
+ required(:title).filled
509
+
510
+ required(:band).schema do
511
+ required(:name).filled
512
+ required(:label).schema do
513
+ required(:location).filled
514
+ end
515
+ end
516
+ end
517
+ end
518
+ end
519
+
520
+ let(:form) { AlbumFormWith1NestedVal.new(album) }
521
+
522
+ it "allows to access dry's result semantics per nested form" do
523
+ form.validate(
524
+ "title" => "",
525
+ "songs" => [{"title" => ""}, {"title" => ""}],
526
+ "band" => {"size" => "", "label" => {"name" => ""}},
527
+ "producers" => [{"name" => ""}, {"name" => ""}, {"name" => "something lovely"}]
528
+ )
529
+
530
+ form.to_result.errors.must_equal(title: ["must be filled"])
531
+ form.band.to_result.errors.must_equal(name: ["must be filled"])
532
+ form.band.label.to_result.errors.must_equal(location: ["must be filled"])
533
+
534
+ # with locale: "de"
535
+ form.to_result.errors(locale: :de).must_equal(title: ["muss abgefüllt sein"])
536
+ form.band.to_result.errors(locale: :de).must_equal(name: ["muss abgefüllt sein"])
537
+ form.band.label.to_result.errors(locale: :de).must_equal(location: ["muss abgefüllt sein"])
538
+ end
539
+ end
540
+ end
541
+
542
+ # describe "same-named group" do
543
+ # class OverwritingForm < TestForm
544
+ # include Reform::Form::Dry::Validations
545
+
546
+ # property :username
547
+ # property :email
548
+
549
+ # validation :email do # FIX ME: is this working for other validator or just bugging here?
550
+ # key(:email, &:filled?) # it's not considered, overitten
551
+ # end
552
+
553
+ # validation :email do # just another group.
554
+ # key(:username, &:filled?)
555
+ # end
556
+ # end
557
+
558
+ # let(:form) { OverwritingForm.new(Session.new) }
559
+
560
+ # # valid.
561
+ # it do
562
+ # form.validate({username: "Helloween"}).must_equal true
563
+ # end
564
+
565
+ # # invalid.
566
+ # it "whoo" do
567
+ # form.validate({}).must_equal false
568
+ # form.errors.messages.inspect.must_equal "{:username=>[\"username can't be blank\"]}"
569
+ # end
570
+ # end
571
+
572
+ describe "inherit: true in same group" do
573
+ class InheritSameGroupForm < TestForm
574
+ property :username
575
+ property :email
576
+ property :full_name, virtual: true
577
+
578
+ validation name: :username do
579
+ params do
580
+ required(:username).filled
581
+ required(:full_name).filled
582
+ end
583
+ end
584
+
585
+ validation name: :username, inherit: true do # extends the above.
586
+ params do
587
+ optional(:username).maybe(:string)
588
+ required(:email).filled
589
+ end
590
+ end
591
+ end
592
+
593
+ let(:form) { InheritSameGroupForm.new(Session.new) }
594
+
595
+ # valid.
596
+ it do
597
+ skip "waiting dry-v to add this as feature https://github.com/dry-rb/dry-schema/issues/33"
598
+ form.validate(email: 9).must_equal true
599
+ end
600
+
601
+ # invalid.
602
+ it do
603
+ skip "waiting dry-v to add this as feature https://github.com/dry-rb/dry-schema/issues/33"
604
+ form.validate({}).must_equal false
605
+ form.errors.messages.must_equal email: ["must be filled"], full_name: ["must be filled"]
606
+ end
607
+ end
608
+
609
+ describe "if: with lambda" do
610
+ class IfWithLambdaForm < TestForm
611
+ property :username
612
+ property :email
613
+ property :password
614
+
615
+ validation name: :email do
616
+ params { required(:email).filled }
617
+ end
618
+
619
+ # run this is :email group is true.
620
+ validation name: :after_email, if: ->(results) { results[:email].success? } do # extends the above.
621
+ params { required(:username).filled }
622
+ end
623
+
624
+ # block gets evaled in form instance context.
625
+ validation name: :password, if: ->(results) { email == "john@trb.org" } do
626
+ params { required(:password).filled }
627
+ end
628
+ end
629
+
630
+ let(:form) { IfWithLambdaForm.new(Session.new) }
631
+
632
+ # valid.
633
+ it do
634
+ form.validate(username: "Strung Out", email: 9).must_equal true
635
+ end
636
+
637
+ # invalid.
638
+ it do
639
+ form.validate(email: 9).must_equal false
640
+ form.errors.messages.inspect.must_equal "{:username=>[\"must be filled\"]}"
641
+ end
642
+ end
643
+
644
+ class NestedSchemaValidationTest < MiniTest::Spec
645
+ AddressSchema = Dry::Schema.Params do
646
+ required(:company).filled(:int?)
647
+ end
648
+
649
+ class OrderForm < TestForm
650
+ property :delivery_address do
651
+ property :company
652
+ end
653
+
654
+ validation do
655
+ params { required(:delivery_address).schema(AddressSchema) }
656
+ end
657
+ end
658
+
659
+ let(:company) { Struct.new(:company) }
660
+ let(:order) { Struct.new(:delivery_address) }
661
+ let(:form) { OrderForm.new(order.new(company.new)) }
662
+
663
+ it "has company error" do
664
+ form.validate(delivery_address: {company: "not int"}).must_equal false
665
+ form.errors.messages.must_equal(:"delivery_address.company" => ["must be an integer"])
666
+ end
667
+ end
668
+
669
+ class NestedSchemaValidationWithFormTest < MiniTest::Spec
670
+ class CompanyForm < TestForm
671
+ property :company
672
+
673
+ validation do
674
+ params { required(:company).filled(:int?) }
675
+ end
676
+ end
677
+
678
+ class OrderFormWithForm < TestForm
679
+ property :delivery_address, form: CompanyForm
680
+ end
681
+
682
+ let(:company) { Struct.new(:company) }
683
+ let(:order) { Struct.new(:delivery_address) }
684
+ let(:form) { OrderFormWithForm.new(order.new(company.new)) }
685
+
686
+ it "has company error" do
687
+ form.validate(delivery_address: {company: "not int"}).must_equal false
688
+ form.errors.messages.must_equal(:"delivery_address.company" => ["must be an integer"])
689
+ end
690
+ end
691
+
692
+ class CollectionPropertyWithCustomRuleTest < MiniTest::Spec
693
+ Artist = Struct.new(:first_name, :last_name)
694
+ Song = Struct.new(:title, :enabled)
695
+ Album = Struct.new(:title, :songs, :artist)
696
+
697
+ class AlbumForm < TestForm
698
+ property :title
699
+
700
+ collection :songs, virtual: true, populate_if_empty: Song do
701
+ property :title
702
+ property :enabled
703
+
704
+ validation do
705
+ params { required(:title).filled }
706
+ end
707
+ end
708
+
709
+ property :artist, populate_if_empty: Artist do
710
+ property :first_name
711
+ property :last_name
712
+ end
713
+
714
+ validation do
715
+ config.messages.load_paths << "test/fixtures/dry_new_api_error_messages.yml"
716
+
717
+ params do
718
+ required(:songs).filled
719
+ required(:artist).filled
720
+ end
721
+
722
+ rule(:songs) do
723
+ key.failure(:a_song?) unless value.any? { |el| el && el[:enabled] }
724
+ end
725
+
726
+ rule(:artist) do
727
+ key.failure(:with_last_name?) unless value[:last_name]
728
+ end
729
+ end
730
+ end
731
+
732
+ it "validates fails and shows the correct errors" do
733
+ form = AlbumForm.new(Album.new(nil, [], nil))
734
+ form.validate(
735
+ "songs" => [
736
+ {"title" => "One", "enabled" => false},
737
+ {"title" => nil, "enabled" => false},
738
+ {"title" => "Three", "enabled" => false}
739
+ ],
740
+ "artist" => {"last_name" => nil}
741
+ ).must_equal false
742
+ form.songs.size.must_equal 3
743
+
744
+ form.errors.messages.must_equal(
745
+ :songs => ["must have at least one enabled song"],
746
+ :artist => ["must have last name"],
747
+ :"songs.title" => ["must be filled"]
748
+ )
749
+ end
750
+ end
751
+
752
+ class DryVWithSchemaAndParams < MiniTest::Spec
753
+ Foo = Struct.new(:age)
754
+
755
+ class ParamsForm < TestForm
756
+ property :age
757
+
758
+ validation do
759
+ params { required(:age).value(:integer) }
760
+
761
+ rule(:age) { key.failure("value exceeded") if value > 999 }
762
+ end
763
+ end
764
+
765
+ class SchemaForm < TestForm
766
+ property :age
767
+
768
+ validation do
769
+ schema { required(:age).value(:integer) }
770
+
771
+ rule(:age) { key.failure("value exceeded") if value > 999 }
772
+ end
773
+ end
774
+
775
+ it "using params" do
776
+ model = Foo.new
777
+ form = ParamsForm.new(model)
778
+ form.validate(age: "99").must_equal true
779
+ form.sync
780
+ model.age.must_equal "99"
781
+
782
+ form = ParamsForm.new(Foo.new)
783
+ form.validate(age: "1000").must_equal false
784
+ form.errors.messages.must_equal age: ["value exceeded"]
785
+ end
786
+
787
+ it "using schema" do
788
+ model = Foo.new
789
+ form = SchemaForm.new(model)
790
+ form.validate(age: "99").must_equal false
791
+ form.validate(age: 99).must_equal true
792
+ form.sync
793
+ model.age.must_equal 99
794
+
795
+ form = SchemaForm.new(Foo.new)
796
+ form.validate(age: 1000).must_equal false
797
+ form.errors.messages.must_equal age: ["value exceeded"]
798
+ end
799
+ end
800
+
801
+ # Currenty dry-v don't support that option, it doesn't make sense
802
+ # I've talked to @solnic and he plans to add a "hint" feature to show
803
+ # more errors messages than only those that have failed.
804
+ #
805
+ # describe "multiple errors for property" do
806
+ # class MultipleErrorsForPropertyForm < TestForm
807
+ # include Reform::Form::Dry::Validations
808
+
809
+ # property :username
810
+
811
+ # validation :default do
812
+ # key(:username) do |username|
813
+ # username.filled? | (username.min_size?(2) & username.max_size?(3))
814
+ # end
815
+ # end
816
+ # end
817
+
818
+ # let(:form) { MultipleErrorsForPropertyForm.new(Session.new) }
819
+
820
+ # # valid.
821
+ # it do
822
+ # form.validate({username: ""}).must_equal false
823
+ # form.errors.messages.inspect.must_equal "{:username=>[\"username must be filled\", \"username is not proper size\"]}"
824
+ # end
825
+ # end
826
+ end