reform 2.2.4

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGES.md +415 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +339 -0
  8. data/Rakefile +15 -0
  9. data/TODO.md +45 -0
  10. data/gemfiles/Gemfile.disposable-0.3 +6 -0
  11. data/lib/reform.rb +8 -0
  12. data/lib/reform/contract.rb +77 -0
  13. data/lib/reform/contract/errors.rb +43 -0
  14. data/lib/reform/contract/validate.rb +33 -0
  15. data/lib/reform/form.rb +94 -0
  16. data/lib/reform/form/call.rb +23 -0
  17. data/lib/reform/form/coercion.rb +3 -0
  18. data/lib/reform/form/composition.rb +34 -0
  19. data/lib/reform/form/dry.rb +67 -0
  20. data/lib/reform/form/module.rb +27 -0
  21. data/lib/reform/form/mongoid.rb +37 -0
  22. data/lib/reform/form/orm.rb +26 -0
  23. data/lib/reform/form/populator.rb +123 -0
  24. data/lib/reform/form/prepopulate.rb +24 -0
  25. data/lib/reform/form/validate.rb +60 -0
  26. data/lib/reform/mongoid.rb +4 -0
  27. data/lib/reform/validation.rb +40 -0
  28. data/lib/reform/validation/groups.rb +73 -0
  29. data/lib/reform/version.rb +3 -0
  30. data/reform.gemspec +29 -0
  31. data/test/benchmarking.rb +26 -0
  32. data/test/call_test.rb +23 -0
  33. data/test/changed_test.rb +41 -0
  34. data/test/coercion_test.rb +66 -0
  35. data/test/composition_test.rb +149 -0
  36. data/test/contract_test.rb +77 -0
  37. data/test/default_test.rb +22 -0
  38. data/test/deprecation_test.rb +27 -0
  39. data/test/deserialize_test.rb +104 -0
  40. data/test/errors_test.rb +165 -0
  41. data/test/feature_test.rb +65 -0
  42. data/test/fixtures/dry_error_messages.yml +44 -0
  43. data/test/form_option_test.rb +24 -0
  44. data/test/form_test.rb +57 -0
  45. data/test/from_test.rb +75 -0
  46. data/test/inherit_test.rb +119 -0
  47. data/test/module_test.rb +142 -0
  48. data/test/parse_pipeline_test.rb +15 -0
  49. data/test/populate_test.rb +270 -0
  50. data/test/populator_skip_test.rb +28 -0
  51. data/test/prepopulator_test.rb +112 -0
  52. data/test/read_only_test.rb +3 -0
  53. data/test/readable_test.rb +30 -0
  54. data/test/readonly_test.rb +14 -0
  55. data/test/reform_test.rb +223 -0
  56. data/test/save_test.rb +89 -0
  57. data/test/setup_test.rb +48 -0
  58. data/test/skip_if_test.rb +74 -0
  59. data/test/skip_setter_and_getter_test.rb +54 -0
  60. data/test/test_helper.rb +49 -0
  61. data/test/validate_test.rb +420 -0
  62. data/test/validation/dry_test.rb +60 -0
  63. data/test/validation/dry_validation_test.rb +352 -0
  64. data/test/validation/errors.yml +4 -0
  65. data/test/virtual_test.rb +24 -0
  66. data/test/writeable_test.rb +29 -0
  67. metadata +265 -0
@@ -0,0 +1,142 @@
1
+ require "test_helper"
2
+ require 'reform/form/coercion'
3
+
4
+ class ModuleInclusionTest < MiniTest::Spec
5
+ module BandPropertyForm
6
+ include Reform::Form::Module
7
+
8
+ property :band do
9
+ property :title
10
+
11
+ validation do
12
+ key(:title).required
13
+ end
14
+
15
+ def id # gets mixed into Form, too.
16
+ 2
17
+ end
18
+ end
19
+
20
+ def id # gets mixed into Form, too.
21
+ 1
22
+ end
23
+
24
+ validation do
25
+ key(:band).required
26
+ end
27
+
28
+ include Dry::Types.module # allows using Types::* in module.
29
+ property :cool, type: Form::Bool # test coercion.
30
+ end
31
+
32
+ # TODO: test if works, move stuff into inherit_schema!
33
+ module AirplaysPropertyForm
34
+ include Reform::Form::Module
35
+
36
+ collection :airplays do
37
+ property :station
38
+ validation do
39
+ key(:station).required
40
+ end
41
+ end
42
+ validation do
43
+ key(:airplays).required
44
+ end
45
+ end
46
+
47
+
48
+ # test:
49
+ # by including BandPropertyForm into multiple classes we assure that options hashes don't get messed up by AM:V.
50
+ class HitForm < Reform::Form
51
+ include BandPropertyForm
52
+ end
53
+
54
+ class SongForm < Reform::Form
55
+ include Coercion
56
+ property :title
57
+
58
+ include BandPropertyForm
59
+ end
60
+
61
+
62
+ let (:song) { OpenStruct.new(:band => OpenStruct.new(:title => "Time Again")) }
63
+
64
+ # nested form from module is present and creates accessor.
65
+ it { SongForm.new(song).band.title.must_equal "Time Again" }
66
+
67
+ # methods from module get included.
68
+ it { SongForm.new(song).id.must_equal 1 }
69
+ it { SongForm.new(song).band.id.must_equal 2 }
70
+
71
+ # validators get inherited.
72
+ it do
73
+ form = SongForm.new(OpenStruct.new)
74
+ form.validate({})
75
+ form.errors.messages.must_equal({:band=>["is missing"]})
76
+ end
77
+
78
+ # coercion works
79
+ it do
80
+ form = SongForm.new(OpenStruct.new)
81
+ form.validate({cool: "1"})
82
+ form.cool.must_equal true
83
+ end
84
+
85
+
86
+ # include a module into a module into a class :)
87
+ module AlbumFormModule
88
+ include Reform::Form::Module
89
+ include BandPropertyForm
90
+
91
+ property :name
92
+ validation do
93
+ key(:name).required
94
+ end
95
+ end
96
+
97
+ class AlbumForm < Reform::Form
98
+ include AlbumFormModule
99
+
100
+ # pp heritage
101
+ property :band, :inherit => true do
102
+ property :label
103
+ validation do
104
+ key(:label).required
105
+ end
106
+ end
107
+ end
108
+
109
+ it do
110
+ form = AlbumForm.new(OpenStruct.new(:band => OpenStruct.new))
111
+ form.validate({"band" => {}})
112
+ form.errors.messages.must_equal({:band=>["must be filled"], :"band.title"=>["is missing"], :"band.label"=>["is missing"], :name=>["is missing"]})
113
+ end
114
+
115
+
116
+ describe "module with custom accessors" do
117
+ module SongModule
118
+ include Reform::Form::Module
119
+
120
+ property :id # no custom accessor for id.
121
+ property :title # has custom accessor.
122
+
123
+ module InstanceMethods
124
+ def title
125
+ super.upcase
126
+ end
127
+ end
128
+ end
129
+
130
+ class IncludingSongForm < Reform::Form
131
+ include SongModule
132
+ end
133
+
134
+ let (:song) { OpenStruct.new(id: 1, title: "Instant Mash") }
135
+
136
+ it do
137
+ IncludingSongForm.new(song).id.must_equal 1
138
+ IncludingSongForm.new(song).title.must_equal "INSTANT MASH"
139
+ end
140
+ end
141
+ end
142
+
@@ -0,0 +1,15 @@
1
+ require "test_helper"
2
+
3
+ class ParsePipelineTest < MiniTest::Spec
4
+ Album = Struct.new(:name)
5
+
6
+ class AlbumForm < Reform::Form
7
+ property :name, deserializer: { parse_pipeline: ->(input, options) { Representable::Pipeline[->(input, options) { options[:represented].name = input.inspect }] } }
8
+ end
9
+
10
+ it "allows passing :parse_pipeline directly" do
11
+ form = AlbumForm.new(Album.new)
12
+ form.validate("name" => "Greatest Hits")
13
+ form.name.must_equal "{\"name\"=>\"Greatest Hits\"}"
14
+ end
15
+ end
@@ -0,0 +1,270 @@
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 < Reform::Form
9
+ property :name, populator: ->(options) { self.name = options[:fragment].reverse }
10
+ validation do
11
+ key(:name).required
12
+ end
13
+
14
+ collection :songs,
15
+ populator: ->(options) {
16
+ fragment, collection, index = options[:fragment], options[:model], options[:index]
17
+
18
+ (item = collection[index]) ? item : collection.insert(index, Song.new) } do
19
+
20
+ property :title
21
+ validation do
22
+ key(:title).required
23
+ end
24
+
25
+ property :composer, populator: ->(options) { options[:model] || self.composer= Artist.new } do
26
+ property :name
27
+ validation do
28
+ key(:name).required
29
+ end
30
+ end
31
+ end
32
+
33
+ # property :artist, populator: lambda { |fragment, options| (item = options.binding.get) ? item : Artist.new } do
34
+ # NOTE: we have to document that model here is the twin!
35
+ property :artist, populator: ->(options) { options[:model] || self.artist = Artist.new } do
36
+ property :name
37
+ end
38
+ end
39
+
40
+ let (:song) { Song.new("Broken") }
41
+ let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
42
+ let (:composer) { Artist.new("Greg Graffin") }
43
+ let (:artist) { Artist.new("Bad Religion") }
44
+ let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
45
+
46
+ let (:form) { AlbumForm.new(album) }
47
+
48
+ it "runs populator on scalar" do
49
+ form.validate(
50
+ "name" => "override me!"
51
+ )
52
+
53
+ form.name.must_equal "!em edirrevo"
54
+ end
55
+
56
+ # changing existing property :artist.
57
+ # TODO: check with artist==nil
58
+ it do
59
+ old_id = artist.object_id
60
+
61
+ form.validate(
62
+ "artist" => {"name" => "Marcus Miller"}
63
+ )
64
+
65
+ form.artist.model.object_id.must_equal old_id
66
+ end
67
+
68
+ # use populator for default value on scalars?
69
+
70
+ # adding to collection via :populator.
71
+ # valid.
72
+ it "yyy" do
73
+ form.validate(
74
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
75
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
76
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
77
+ ).must_equal true
78
+
79
+ form.errors.messages.inspect.must_equal "{}"
80
+
81
+ # form has updated.
82
+ form.name.must_equal "The Dissent Of Man"
83
+ form.songs[0].title.must_equal "Fallout"
84
+ form.songs[1].title.must_equal "Roxanne"
85
+ form.songs[1].composer.name.must_equal "Greg Graffin"
86
+
87
+ form.songs[1].composer.model.must_be_instance_of Artist
88
+
89
+ form.songs[1].title.must_equal "Roxanne"
90
+ form.songs[2].title.must_equal "Rime Of The Ancient Mariner" # new song added.
91
+ form.songs[3].title.must_equal "Re-Education"
92
+ form.songs[3].composer.name.must_equal "Rise Against"
93
+ form.songs.size.must_equal 4
94
+ form.artist.name.must_equal "Bad Religion"
95
+
96
+
97
+ # model has not changed, yet.
98
+ album.name.must_equal "The Dissent Of Man"
99
+ album.songs[0].title.must_equal "Broken"
100
+ album.songs[1].title.must_equal "Resist Stance"
101
+ album.songs[1].composer.name.must_equal "Greg Graffin"
102
+ album.songs.size.must_equal 2
103
+ album.artist.name.must_equal "Bad Religion"
104
+ end
105
+ end
106
+
107
+ class PopulateWithMethodTest < Minitest::Spec
108
+ Album = Struct.new(:title)
109
+
110
+ class AlbumForm < Reform::Form
111
+ property :title, populator: :title!
112
+
113
+ def title!(options)
114
+ self.title = options[:fragment].reverse
115
+ end
116
+ end
117
+
118
+ let (:form) { AlbumForm.new(Album.new) }
119
+
120
+ it "runs populator method" do
121
+ form.validate(
122
+ "title" => "override me!"
123
+ )
124
+
125
+ form.title.must_equal "!em edirrevo"
126
+ end
127
+ end
128
+
129
+ class PopulateIfEmptyTest < MiniTest::Spec
130
+ Song = Struct.new(:title, :album, :composer)
131
+ Album = Struct.new(:name, :songs, :artist)
132
+ Artist = Struct.new(:name)
133
+
134
+ let (:song) { Song.new("Broken") }
135
+ let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
136
+ let (:composer) { Artist.new("Greg Graffin") }
137
+ let (:artist) { Artist.new("Bad Religion") }
138
+ let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
139
+
140
+
141
+
142
+ class AlbumForm < Reform::Form
143
+ property :name
144
+
145
+ collection :songs,
146
+ populate_if_empty: Song do # class name works.
147
+
148
+ property :title
149
+ validation do
150
+ key(:title).required
151
+ end
152
+
153
+ property :composer, populate_if_empty: :populate_composer! do # lambda works, too. in form context.
154
+ property :name
155
+ validation do
156
+ key(:name).required
157
+ end
158
+ end
159
+
160
+ private
161
+ def populate_composer!(fragment, options)
162
+ Artist.new
163
+ end
164
+ end
165
+
166
+ property :artist, populate_if_empty: lambda { |args| create_artist(args[:fragment], args[:user_options]) } do # methods work, too.
167
+ property :name
168
+ end
169
+
170
+ private
171
+ class Sting < Artist
172
+ attr_accessor :args
173
+ end
174
+ def create_artist(input, user_options)
175
+ Sting.new.tap { |artist| artist.args=([input, user_options].to_s) }
176
+ end
177
+ end
178
+
179
+ let (:form) { AlbumForm.new(album) }
180
+
181
+ it do
182
+ form.songs.size.must_equal 2
183
+
184
+ form.validate(
185
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
186
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
187
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
188
+ ).must_equal true
189
+
190
+ form.errors.messages.inspect.must_equal "{}"
191
+
192
+ # form has updated.
193
+ form.name.must_equal "The Dissent Of Man"
194
+ form.songs[0].title.must_equal "Fallout"
195
+ form.songs[1].title.must_equal "Roxanne"
196
+ form.songs[1].composer.name.must_equal "Greg Graffin"
197
+ form.songs[1].title.must_equal "Roxanne"
198
+ form.songs[2].title.must_equal "Rime Of The Ancient Mariner" # new song added.
199
+ form.songs[3].title.must_equal "Re-Education"
200
+ form.songs[3].composer.name.must_equal "Rise Against"
201
+ form.songs.size.must_equal 4
202
+ form.artist.name.must_equal "Bad Religion"
203
+
204
+
205
+ # model has not changed, yet.
206
+ album.name.must_equal "The Dissent Of Man"
207
+ album.songs[0].title.must_equal "Broken"
208
+ album.songs[1].title.must_equal "Resist Stance"
209
+ album.songs[1].composer.name.must_equal "Greg Graffin"
210
+ album.songs.size.must_equal 2
211
+ album.artist.name.must_equal "Bad Religion"
212
+ end
213
+
214
+ # trigger artist populator. lambda calling form instance method.
215
+ it "xxxx" do
216
+ form = AlbumForm.new(album = Album.new)
217
+ form.validate("artist" => {"name" => "From Autumn To Ashes"})
218
+
219
+ form.artist.name.must_equal "From Autumn To Ashes"
220
+ # test lambda was executed in form context.
221
+ form.artist.model.must_be_instance_of AlbumForm::Sting
222
+ # test lambda block arguments.
223
+ form.artist.model.args.to_s.must_equal "[{\"name\"=>\"From Autumn To Ashes\"}, nil]"
224
+
225
+ album.artist.must_equal nil
226
+ end
227
+ end
228
+
229
+
230
+ # delete songs while deserializing.
231
+ class PopulateIfEmptyWithDeletionTest < MiniTest::Spec
232
+ Song = Struct.new(:title, :album, :composer)
233
+ Album = Struct.new(:name, :songs, :artist)
234
+
235
+ let (:song) { Song.new("Broken") }
236
+ let (:song2) { Song.new("Resist Stance") }
237
+ let (:album) { Album.new("The Dissent Of Man", [song, song2]) }
238
+
239
+
240
+ class AlbumForm < Reform::Form
241
+ property :name
242
+
243
+ collection :songs,
244
+ populate_if_empty: Song, skip_if: :delete_song! do
245
+
246
+ property :title
247
+ validation do
248
+ key(:title).required
249
+ end
250
+ end
251
+
252
+ def delete_song!(options)
253
+ songs.delete(songs[0]) and return true if options[:fragment]["title"] == "Broken, delete me!"
254
+ false
255
+ end
256
+ end
257
+
258
+ let (:form) { AlbumForm.new(album) }
259
+
260
+ it do
261
+ form.validate(
262
+ "songs" => [{"title" => "Broken, delete me!"}, {"title" => "Roxanne"}]
263
+ ).must_equal true
264
+
265
+ form.errors.messages.inspect.must_equal "{}"
266
+
267
+ form.songs.size.must_equal 1
268
+ form.songs[0].title.must_equal "Roxanne"
269
+ end
270
+ end
@@ -0,0 +1,28 @@
1
+ require "test_helper"
2
+
3
+ class PopulatorSkipTest < MiniTest::Spec
4
+ Album = Struct.new(:songs)
5
+ Song = Struct.new(:title)
6
+
7
+
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
15
+ end
16
+ end
17
+
18
+ it do
19
+ form = AlbumForm.new(Album.new([Song.new, Song.new]))
20
+ hash = {songs: [{title: "Good"}, {title: "Bad"}]}
21
+
22
+ form.validate(hash)
23
+
24
+ form.songs.size.must_equal 2
25
+ form.songs[0].title.must_equal nil
26
+ form.songs[1].title.must_equal "Bad"
27
+ end
28
+ end