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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +11 -0
- data/CHANGES.md +415 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +339 -0
- data/Rakefile +15 -0
- data/TODO.md +45 -0
- data/gemfiles/Gemfile.disposable-0.3 +6 -0
- data/lib/reform.rb +8 -0
- data/lib/reform/contract.rb +77 -0
- data/lib/reform/contract/errors.rb +43 -0
- data/lib/reform/contract/validate.rb +33 -0
- data/lib/reform/form.rb +94 -0
- data/lib/reform/form/call.rb +23 -0
- data/lib/reform/form/coercion.rb +3 -0
- data/lib/reform/form/composition.rb +34 -0
- data/lib/reform/form/dry.rb +67 -0
- data/lib/reform/form/module.rb +27 -0
- data/lib/reform/form/mongoid.rb +37 -0
- data/lib/reform/form/orm.rb +26 -0
- data/lib/reform/form/populator.rb +123 -0
- data/lib/reform/form/prepopulate.rb +24 -0
- data/lib/reform/form/validate.rb +60 -0
- data/lib/reform/mongoid.rb +4 -0
- data/lib/reform/validation.rb +40 -0
- data/lib/reform/validation/groups.rb +73 -0
- data/lib/reform/version.rb +3 -0
- data/reform.gemspec +29 -0
- data/test/benchmarking.rb +26 -0
- data/test/call_test.rb +23 -0
- data/test/changed_test.rb +41 -0
- data/test/coercion_test.rb +66 -0
- data/test/composition_test.rb +149 -0
- data/test/contract_test.rb +77 -0
- data/test/default_test.rb +22 -0
- data/test/deprecation_test.rb +27 -0
- data/test/deserialize_test.rb +104 -0
- data/test/errors_test.rb +165 -0
- data/test/feature_test.rb +65 -0
- data/test/fixtures/dry_error_messages.yml +44 -0
- data/test/form_option_test.rb +24 -0
- data/test/form_test.rb +57 -0
- data/test/from_test.rb +75 -0
- data/test/inherit_test.rb +119 -0
- data/test/module_test.rb +142 -0
- data/test/parse_pipeline_test.rb +15 -0
- data/test/populate_test.rb +270 -0
- data/test/populator_skip_test.rb +28 -0
- data/test/prepopulator_test.rb +112 -0
- data/test/read_only_test.rb +3 -0
- data/test/readable_test.rb +30 -0
- data/test/readonly_test.rb +14 -0
- data/test/reform_test.rb +223 -0
- data/test/save_test.rb +89 -0
- data/test/setup_test.rb +48 -0
- data/test/skip_if_test.rb +74 -0
- data/test/skip_setter_and_getter_test.rb +54 -0
- data/test/test_helper.rb +49 -0
- data/test/validate_test.rb +420 -0
- data/test/validation/dry_test.rb +60 -0
- data/test/validation/dry_validation_test.rb +352 -0
- data/test/validation/errors.yml +4 -0
- data/test/virtual_test.rb +24 -0
- data/test/writeable_test.rb +29 -0
- metadata +265 -0
data/test/module_test.rb
ADDED
@@ -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
|