reform 2.2.4

Sign up to get free protection for your applications and to get access to all the features.
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