reform 2.2.4 → 2.3.3

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 (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,304 @@
1
+ require "test_helper"
2
+
3
+ class PopulatorTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ class AlbumForm < TestForm
9
+ property :name, populator: ->(options) { self.name = options[:fragment].reverse }
10
+ validation do
11
+ required(:name).filled
12
+ end
13
+
14
+ collection :songs,
15
+ populator: ->(fragment:, model:, index:, **) {
16
+ (item = model[index]) ? item : model.insert(index, Song.new)
17
+ } do
18
+ property :title
19
+ validation do
20
+ required(:title).filled
21
+ end
22
+
23
+ property :composer, populator: ->(options) { options[:model] || self.composer = Artist.new } do
24
+ property :name
25
+ validation do
26
+ required(:name).filled
27
+ end
28
+ end
29
+ end
30
+
31
+ # property :artist, populator: lambda { |fragment, options| (item = options.binding.get) ? item : Artist.new } do
32
+ # NOTE: we have to document that model here is the twin!
33
+ property :artist, populator: ->(options) { options[:model] || self.artist = Artist.new } do
34
+ property :name
35
+ end
36
+ end
37
+
38
+ let(:song) { Song.new("Broken") }
39
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
40
+ let(:composer) { Artist.new("Greg Graffin") }
41
+ let(:artist) { Artist.new("Bad Religion") }
42
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
43
+
44
+ let(:form) { AlbumForm.new(album) }
45
+
46
+ it "runs populator on scalar" do
47
+ form.validate(
48
+ "name" => "override me!"
49
+ )
50
+
51
+ _(form.name).must_equal "!em edirrevo"
52
+ end
53
+
54
+ # changing existing property :artist.
55
+ # TODO: check with artist==nil
56
+ it do
57
+ old_id = artist.object_id
58
+
59
+ form.validate(
60
+ "artist" => {"name" => "Marcus Miller"}
61
+ )
62
+
63
+ _(form.artist.model.object_id).must_equal old_id
64
+ end
65
+
66
+ # use populator for default value on scalars?
67
+
68
+ # adding to collection via :populator.
69
+ # valid.
70
+ it "yyy" do
71
+ _(form.validate(
72
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
73
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
74
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
75
+ )).must_equal true
76
+
77
+ _(form.errors.messages.inspect).must_equal "{}"
78
+
79
+ # form has updated.
80
+ _(form.name).must_equal "The Dissent Of Man"
81
+ _(form.songs[0].title).must_equal "Fallout"
82
+ _(form.songs[1].title).must_equal "Roxanne"
83
+ _(form.songs[1].composer.name).must_equal "Greg Graffin"
84
+
85
+ _(form.songs[1].composer.model).must_be_instance_of Artist
86
+
87
+ _(form.songs[1].title).must_equal "Roxanne"
88
+ _(form.songs[2].title).must_equal "Rime Of The Ancient Mariner" # new song added.
89
+ _(form.songs[3].title).must_equal "Re-Education"
90
+ _(form.songs[3].composer.name).must_equal "Rise Against"
91
+ _(form.songs.size).must_equal 4
92
+ _(form.artist.name).must_equal "Bad Religion"
93
+
94
+ # model has not changed, yet.
95
+ _(album.name).must_equal "The Dissent Of Man"
96
+ _(album.songs[0].title).must_equal "Broken"
97
+ _(album.songs[1].title).must_equal "Resist Stance"
98
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
99
+ _(album.songs.size).must_equal 2
100
+ _(album.artist.name).must_equal "Bad Religion"
101
+ end
102
+ end
103
+
104
+ class PopulateWithMethodTest < Minitest::Spec
105
+ Album = Struct.new(:title)
106
+
107
+ class AlbumForm < TestForm
108
+ property :title, populator: :title!
109
+
110
+ def title!(options)
111
+ self.title = options[:fragment].reverse
112
+ end
113
+ end
114
+
115
+ let(:form) { AlbumForm.new(Album.new) }
116
+
117
+ it "runs populator method" do
118
+ form.validate("title" => "override me!")
119
+
120
+ _(form.title).must_equal "!em edirrevo"
121
+ end
122
+ end
123
+
124
+ class PopulateWithCallableTest < Minitest::Spec
125
+ Album = Struct.new(:title)
126
+
127
+ class TitlePopulator
128
+ include Uber::Callable
129
+
130
+ def call(form, options)
131
+ form.title = options[:fragment].reverse
132
+ end
133
+ end
134
+
135
+ class AlbumForm < TestForm
136
+ property :title, populator: TitlePopulator.new
137
+ end
138
+
139
+ let(:form) { AlbumForm.new(Album.new) }
140
+
141
+ it "runs populator method" do
142
+ form.validate("title" => "override me!")
143
+
144
+ _(form.title).must_equal "!em edirrevo"
145
+ end
146
+ end
147
+
148
+ class PopulateWithProcTest < Minitest::Spec
149
+ Album = Struct.new(:title)
150
+
151
+ TitlePopulator = ->(options) do
152
+ options[:represented].title = options[:fragment].reverse
153
+ end
154
+
155
+ class AlbumForm < TestForm
156
+ property :title, populator: TitlePopulator
157
+ end
158
+
159
+ let(:form) { AlbumForm.new(Album.new) }
160
+
161
+ it "runs populator method" do
162
+ form.validate("title" => "override me!")
163
+
164
+ _(form.title).must_equal "!em edirrevo"
165
+ end
166
+ end
167
+
168
+ class PopulateIfEmptyTest < MiniTest::Spec
169
+ Song = Struct.new(:title, :album, :composer)
170
+ Album = Struct.new(:name, :songs, :artist)
171
+ Artist = Struct.new(:name)
172
+
173
+ let(:song) { Song.new("Broken") }
174
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
175
+ let(:composer) { Artist.new("Greg Graffin") }
176
+ let(:artist) { Artist.new("Bad Religion") }
177
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
178
+
179
+ class AlbumForm < TestForm
180
+ property :name
181
+
182
+ collection :songs,
183
+ populate_if_empty: Song do # class name works.
184
+
185
+ property :title
186
+ validation do
187
+ required(:title).filled
188
+ end
189
+
190
+ property :composer, populate_if_empty: :populate_composer! do # lambda works, too. in form context.
191
+ property :name
192
+ validation do
193
+ required(:name).filled
194
+ end
195
+ end
196
+
197
+ private
198
+ def populate_composer!(options)
199
+ Artist.new
200
+ end
201
+ end
202
+
203
+ property :artist, populate_if_empty: ->(args) { create_artist(args[:fragment], args[:user_options]) } do # methods work, too.
204
+ property :name
205
+ end
206
+
207
+ private
208
+ class Sting < Artist
209
+ attr_accessor :args
210
+ end
211
+ def create_artist(input, user_options)
212
+ Sting.new.tap { |artist| artist.args = ([input, user_options].to_s) }
213
+ end
214
+ end
215
+
216
+ let(:form) { AlbumForm.new(album) }
217
+
218
+ it do
219
+ _(form.songs.size).must_equal 2
220
+
221
+ _(form.validate(
222
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
223
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
224
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
225
+ )).must_equal true
226
+
227
+ _(form.errors.messages.inspect).must_equal "{}"
228
+
229
+ # form has updated.
230
+ _(form.name).must_equal "The Dissent Of Man"
231
+ _(form.songs[0].title).must_equal "Fallout"
232
+ _(form.songs[1].title).must_equal "Roxanne"
233
+ _(form.songs[1].composer.name).must_equal "Greg Graffin"
234
+ _(form.songs[1].title).must_equal "Roxanne"
235
+ _(form.songs[2].title).must_equal "Rime Of The Ancient Mariner" # new song added.
236
+ _(form.songs[3].title).must_equal "Re-Education"
237
+ _(form.songs[3].composer.name).must_equal "Rise Against"
238
+ _(form.songs.size).must_equal 4
239
+ _(form.artist.name).must_equal "Bad Religion"
240
+
241
+ # model has not changed, yet.
242
+ _(album.name).must_equal "The Dissent Of Man"
243
+ _(album.songs[0].title).must_equal "Broken"
244
+ _(album.songs[1].title).must_equal "Resist Stance"
245
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
246
+ _(album.songs.size).must_equal 2
247
+ _(album.artist.name).must_equal "Bad Religion"
248
+ end
249
+
250
+ # trigger artist populator. lambda calling form instance method.
251
+ it "xxxx" do
252
+ form = AlbumForm.new(album = Album.new)
253
+ form.validate("artist" => {"name" => "From Autumn To Ashes"})
254
+
255
+ _(form.artist.name).must_equal "From Autumn To Ashes"
256
+ # test lambda was executed in form context.
257
+ _(form.artist.model).must_be_instance_of AlbumForm::Sting
258
+ # test lambda block arguments.
259
+ _(form.artist.model.args.to_s).must_equal "[{\"name\"=>\"From Autumn To Ashes\"}, nil]"
260
+
261
+ assert_nil album.artist
262
+ end
263
+ end
264
+
265
+ # delete songs while deserializing.
266
+ class PopulateIfEmptyWithDeletionTest < MiniTest::Spec
267
+ Song = Struct.new(:title, :album, :composer)
268
+ Album = Struct.new(:name, :songs, :artist)
269
+
270
+ let(:song) { Song.new("Broken") }
271
+ let(:song2) { Song.new("Resist Stance") }
272
+ let(:album) { Album.new("The Dissent Of Man", [song, song2]) }
273
+
274
+ class AlbumForm < TestForm
275
+ property :name
276
+
277
+ collection :songs,
278
+ populate_if_empty: Song, skip_if: :delete_song! do
279
+
280
+ property :title
281
+ validation do
282
+ required(:title).filled
283
+ end
284
+ end
285
+
286
+ def delete_song!(options)
287
+ songs.delete(songs[0]) and return true if options[:fragment]["title"] == "Broken, delete me!"
288
+ false
289
+ end
290
+ end
291
+
292
+ let(:form) { AlbumForm.new(album) }
293
+
294
+ it do
295
+ _(form.validate(
296
+ "songs" => [{"title" => "Broken, delete me!"}, {"title" => "Roxanne"}]
297
+ )).must_equal true
298
+
299
+ _(form.errors.messages.inspect).must_equal "{}"
300
+
301
+ _(form.songs.size).must_equal 1
302
+ _(form.songs[0].title).must_equal "Roxanne"
303
+ end
304
+ end
@@ -4,14 +4,14 @@ class PopulatorSkipTest < MiniTest::Spec
4
4
  Album = Struct.new(:songs)
5
5
  Song = Struct.new(:title)
6
6
 
7
+ class AlbumForm < TestForm
8
+ collection :songs, populator: :my_populator do
9
+ property :title
10
+ end
7
11
 
8
- class AlbumForm < Reform::Form
9
- collection :songs,
10
- populator: ->(options) {
11
- return skip! if options[:fragment][:title] == "Good"
12
- songs[options[:index]]
13
- } do
14
- property :title
12
+ def my_populator(options)
13
+ return skip! if options[:fragment][:title] == "Good"
14
+ songs[options[:index]]
15
15
  end
16
16
  end
17
17
 
@@ -21,8 +21,8 @@ class PopulatorSkipTest < MiniTest::Spec
21
21
 
22
22
  form.validate(hash)
23
23
 
24
- form.songs.size.must_equal 2
25
- form.songs[0].title.must_equal nil
26
- form.songs[1].title.must_equal "Bad"
24
+ _(form.songs.size).must_equal 2
25
+ assert_nil form.songs[0].title
26
+ _(form.songs[1].title).must_equal "Bad"
27
27
  end
28
- end
28
+ end
@@ -1,17 +1,17 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
3
  class PrepopulatorTest < MiniTest::Spec
4
4
  Song = Struct.new(:title, :band, :length)
5
5
  Band = Struct.new(:name)
6
6
 
7
- class AlbumForm < Reform::Form
8
- property :title, prepopulator: ->(*){ self.title = "Another Day At Work" } # normal assignment.
7
+ class AlbumForm < TestForm
8
+ property :title, prepopulator: ->(*) { self.title = "Another Day At Work" } # normal assignment.
9
9
  property :length
10
10
 
11
11
  property :hit, prepopulator: ->(options) { self.hit = Song.new(options[:title]) } do # use user options.
12
12
  property :title
13
13
 
14
- property :band, prepopulator: ->(options){ self.band = my_band(options[:title]) } do # invoke your own code.
14
+ property :band, prepopulator: ->(options) { self.band = my_band(options[:title]) } do # invoke your own code.
15
15
  property :name
16
16
  end
17
17
 
@@ -24,7 +24,7 @@ class PrepopulatorTest < MiniTest::Spec
24
24
  property :title
25
25
  end
26
26
 
27
- private
27
+ private
28
28
  def prepopulate_songs!(options)
29
29
  if songs == nil
30
30
  self.songs = [Song.new, Song.new]
@@ -37,25 +37,25 @@ class PrepopulatorTest < MiniTest::Spec
37
37
  it do
38
38
  form = AlbumForm.new(OpenStruct.new(length: 1)).prepopulate!(title: "Potemkin City Limits")
39
39
 
40
- form.length.must_equal 1
41
- form.title.must_equal "Another Day At Work"
42
- form.hit.model.must_equal Song.new("Potemkin City Limits")
43
- form.songs.size.must_equal 2
44
- form.songs[0].model.must_equal Song.new
45
- form.songs[1].model.must_equal Song.new
46
- form.songs[1].model.must_equal Song.new
40
+ _(form.length).must_equal 1
41
+ _(form.title).must_equal "Another Day At Work"
42
+ _(form.hit.model).must_equal Song.new("Potemkin City Limits")
43
+ _(form.songs.size).must_equal 2
44
+ _(form.songs[0].model).must_equal Song.new
45
+ _(form.songs[1].model).must_equal Song.new
46
+ _(form.songs[1].model).must_equal Song.new
47
47
  # prepopulate works more than 1 level, recursive.
48
48
  # it also passes options properly down there.
49
- form.hit.band.model.must_equal Band.new("Potemkin City Limits")
49
+ _(form.hit.band.model).must_equal Band.new("Potemkin City Limits")
50
50
  end
51
51
 
52
52
  # add to existing collection.
53
53
  it do
54
54
  form = AlbumForm.new(OpenStruct.new(songs: [Song.new])).prepopulate!
55
55
 
56
- form.songs.size.must_equal 2
57
- form.songs[0].model.must_equal Song.new
58
- form.songs[1].model.must_equal Song.new
56
+ _(form.songs.size).must_equal 2
57
+ _(form.songs[0].model).must_equal Song.new
58
+ _(form.songs[1].model).must_equal Song.new
59
59
  end
60
60
  end
61
61
 
@@ -63,7 +63,7 @@ end
63
63
  class PrepopulateWithoutConfiguration < MiniTest::Spec
64
64
  Song = Struct.new(:title)
65
65
 
66
- class AlbumForm < Reform::Form
66
+ class AlbumForm < TestForm
67
67
  collection :songs do
68
68
  property :title
69
69
  end
@@ -75,15 +75,14 @@ class PrepopulateWithoutConfiguration < MiniTest::Spec
75
75
 
76
76
  subject { AlbumForm.new(OpenStruct.new(songs: [], hit: nil)).prepopulate! }
77
77
 
78
- it { subject.songs.size.must_equal 0 }
78
+ it { _(subject.songs.size).must_equal 0 }
79
79
  end
80
80
 
81
-
82
81
  class ManualPrepopulatorOverridingTest < MiniTest::Spec
83
82
  Song = Struct.new(:title, :band, :length)
84
83
  Band = Struct.new(:name)
85
84
 
86
- class AlbumForm < Reform::Form
85
+ class AlbumForm < TestForm
87
86
  property :title
88
87
  property :length
89
88
 
@@ -105,8 +104,8 @@ class ManualPrepopulatorOverridingTest < MiniTest::Spec
105
104
  it do
106
105
  form = AlbumForm.new(OpenStruct.new(length: 1)).prepopulate!(title: "Potemkin City Limits")
107
106
 
108
- form.length.must_equal 1
109
- form.hit.model.must_equal Song.new("Potemkin City Limits")
110
- form.hit.title.must_equal "Potemkin City Limits"
107
+ _(form.length).must_equal 1
108
+ _(form.hit.model).must_equal Song.new("Potemkin City Limits")
109
+ _(form.hit.title).must_equal "Potemkin City Limits"
111
110
  end
112
- end
111
+ end
@@ -1,3 +1,14 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
+ class ReadonlyTest < MiniTest::Spec
4
+ class SongForm < TestForm
5
+ property :artist
6
+ property :title, writeable: false
7
+ # TODO: what to do with virtual values?
8
+ end
3
9
 
10
+ let(:form) { SongForm.new(OpenStruct.new) }
11
+
12
+ it { _(form.readonly?(:artist)).must_equal false }
13
+ it { _(form.readonly?(:title)).must_equal true }
14
+ end