reform 2.2.4 → 2.3.3

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