reform 2.3.0.rc1 → 2.3.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.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