reform 1.1.1 → 1.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +35 -1
- data/Gemfile +1 -1
- data/README.md +83 -21
- data/TODO.md +8 -0
- data/database.sqlite3 +0 -0
- data/gemfiles/Gemfile.rails-4.0 +1 -0
- data/lib/reform.rb +4 -2
- data/lib/reform/active_record.rb +2 -1
- data/lib/reform/composition.rb +2 -2
- data/lib/reform/contract.rb +24 -7
- data/lib/reform/contract/setup.rb +21 -9
- data/lib/reform/contract/validate.rb +0 -6
- data/lib/reform/form.rb +6 -8
- data/lib/reform/form/active_model.rb +3 -2
- data/lib/reform/form/active_model/model_validations.rb +13 -1
- data/lib/reform/form/active_record.rb +1 -7
- data/lib/reform/form/changed.rb +9 -0
- data/lib/reform/form/json.rb +13 -0
- data/lib/reform/form/model_reflections.rb +18 -0
- data/lib/reform/form/save.rb +25 -3
- data/lib/reform/form/scalar.rb +4 -2
- data/lib/reform/form/sync.rb +82 -12
- data/lib/reform/form/validate.rb +38 -0
- data/lib/reform/rails.rb +1 -1
- data/lib/reform/representer.rb +14 -23
- data/lib/reform/schema.rb +23 -0
- data/lib/reform/twin.rb +20 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +2 -2
- data/test/active_model_test.rb +2 -2
- data/test/active_record_test.rb +7 -4
- data/test/changed_test.rb +69 -0
- data/test/custom_validation_test.rb +47 -0
- data/test/deserialize_test.rb +2 -7
- data/test/empty_test.rb +30 -0
- data/test/fields_test.rb +24 -0
- data/test/form_composition_test.rb +24 -2
- data/test/form_test.rb +84 -0
- data/test/inherit_test.rb +12 -0
- data/test/model_reflections_test.rb +65 -0
- data/test/read_only_test.rb +28 -0
- data/test/reform_test.rb +2 -175
- data/test/representer_test.rb +47 -0
- data/test/save_test.rb +51 -1
- data/test/scalar_test.rb +0 -18
- data/test/skip_if_test.rb +62 -0
- data/test/skip_unchanged_test.rb +86 -0
- data/test/sync_option_test.rb +83 -0
- data/test/twin_test.rb +23 -0
- data/test/validate_test.rb +9 -1
- metadata +37 -9
- data/lib/reform/form/virtual_attributes.rb +0 -22
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# Reform::ModelReflections will be the interface between the form object and form builders like simple_form.
|
4
|
+
class ModelReflectionTest < MiniTest::Spec
|
5
|
+
class SongForm < Reform::Form
|
6
|
+
include Reform::Form::ActiveRecord
|
7
|
+
include Reform::Form::ModelReflections
|
8
|
+
|
9
|
+
model :song
|
10
|
+
|
11
|
+
property :title
|
12
|
+
property :artist do
|
13
|
+
property :name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ColumnForAttribute
|
18
|
+
def column_for_attribute(*args)
|
19
|
+
"#{self.class}: #{args.inspect}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#column_for_attribute" do
|
24
|
+
let (:artist) { Artist.new }
|
25
|
+
let (:song) { Song.new(artist: artist) }
|
26
|
+
let (:form) { SongForm.new(song) }
|
27
|
+
|
28
|
+
# delegate to model.
|
29
|
+
it do
|
30
|
+
song.extend(ColumnForAttribute)
|
31
|
+
artist.extend(ColumnForAttribute)
|
32
|
+
|
33
|
+
form.column_for_attribute(:title).must_equal "Song: [:title]"
|
34
|
+
form.artist.column_for_attribute(:name).must_equal "Artist: [:name]"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
class SongWithArtistForm < Reform::Form
|
40
|
+
include Reform::Form::ActiveRecord
|
41
|
+
include Reform::Form::ModelReflections
|
42
|
+
include Reform::Form::Composition
|
43
|
+
|
44
|
+
model :artist
|
45
|
+
|
46
|
+
property :name, on: :artist
|
47
|
+
property :title, on: :song
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#column_for_attribute with composition" do
|
51
|
+
let (:artist) { Artist.new }
|
52
|
+
let (:song) { Song.new }
|
53
|
+
let (:form) { SongWithArtistForm.new(artist: artist, song: song) }
|
54
|
+
|
55
|
+
# delegates to respective model.
|
56
|
+
it do
|
57
|
+
song.extend(ColumnForAttribute)
|
58
|
+
artist.extend(ColumnForAttribute)
|
59
|
+
|
60
|
+
|
61
|
+
form.column_for_attribute(:name).must_equal "Artist: [:name]"
|
62
|
+
form.column_for_attribute(:title).must_equal "Song: [:title]"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ReadonlyAttributesTest < MiniTest::Spec
|
4
|
+
Location = Struct.new(:country)
|
5
|
+
|
6
|
+
class LocationForm < Reform::Form
|
7
|
+
property :country, virtual: true # read_only: true
|
8
|
+
end
|
9
|
+
|
10
|
+
let (:loc) { Location.new("Australia") }
|
11
|
+
let (:form) { LocationForm.new(loc) }
|
12
|
+
|
13
|
+
it { form.country.must_equal "Australia" }
|
14
|
+
it do
|
15
|
+
form.validate("country" => "Germany") # this usually won't change when submitting.
|
16
|
+
form.country.must_equal "Germany"
|
17
|
+
|
18
|
+
form.sync
|
19
|
+
loc.country.must_equal "Australia" # the writer wasn't called.
|
20
|
+
|
21
|
+
hash = {}
|
22
|
+
form.save do |nested|
|
23
|
+
hash = nested
|
24
|
+
end
|
25
|
+
|
26
|
+
hash.must_equal("country"=> "Germany")
|
27
|
+
end
|
28
|
+
end
|
data/test/reform_test.rb
CHANGED
@@ -1,56 +1,5 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class RepresenterTest < MiniTest::Spec
|
4
|
-
class SongRepresenter < Reform::Representer
|
5
|
-
property :title
|
6
|
-
property :name
|
7
|
-
end
|
8
|
-
|
9
|
-
let (:rpr) { SongRepresenter.new(Object.new) }
|
10
|
-
|
11
|
-
describe "#fields" do
|
12
|
-
it "returns all properties as strings" do
|
13
|
-
rpr.fields.must_equal(["title", "name"])
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
class WithOptionsTest < MiniTest::Spec
|
19
|
-
subject { Reform::Representer::WithOptions::Options.new }
|
20
|
-
|
21
|
-
it { subject.must_equal({}) }
|
22
|
-
it { subject.exclude!([:id, :title]).must_equal(:exclude => [:id, :title]) }
|
23
|
-
it do
|
24
|
-
subject.exclude!([:id, :title])
|
25
|
-
subject.exclude!([:id, :name])
|
26
|
-
subject.must_equal(:exclude => [:id, :title, :id, :name])
|
27
|
-
end
|
28
|
-
it { subject.include!([:id, :title]).must_equal(:include => [:id, :title]) }
|
29
|
-
end
|
30
|
-
|
31
|
-
class FieldsTest < MiniTest::Spec
|
32
|
-
describe "#new" do
|
33
|
-
it "accepts list of properties" do
|
34
|
-
fields = Reform::Contract::Fields.new([:name, :title])
|
35
|
-
fields.name.must_equal nil
|
36
|
-
fields.title.must_equal nil
|
37
|
-
end
|
38
|
-
|
39
|
-
it "accepts list of properties and values" do
|
40
|
-
fields = Reform::Contract::Fields.new(["name", "title"], "title" => "The Body")
|
41
|
-
fields.name.must_equal nil
|
42
|
-
fields.title.must_equal "The Body"
|
43
|
-
end
|
44
|
-
|
45
|
-
it "processes value syms" do
|
46
|
-
skip "we don't need to test this as representer.to_hash always returns strings"
|
47
|
-
fields = Reform::Fields.new(["name", "title"], :title => "The Body")
|
48
|
-
fields.name.must_equal nil
|
49
|
-
fields.title.must_equal "The Body"
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
3
|
class ReformTest < ReformSpec
|
55
4
|
let (:comp) { OpenStruct.new(:name => "Duran Duran", :title => "Rio") }
|
56
5
|
|
@@ -74,8 +23,8 @@ class ReformTest < ReformSpec
|
|
74
23
|
subject do
|
75
24
|
opts = options
|
76
25
|
Class.new(Reform::Form) do
|
77
|
-
properties
|
78
|
-
properties
|
26
|
+
properties :name, :title, opts
|
27
|
+
properties :created_at
|
79
28
|
end.new(comp)
|
80
29
|
end
|
81
30
|
|
@@ -247,128 +196,6 @@ class ReformTest < ReformSpec
|
|
247
196
|
form.errors.messages.must_equal({:name=>["can't be blank"], :position=>["can't be blank"]})
|
248
197
|
end
|
249
198
|
end
|
250
|
-
|
251
|
-
describe "#column_for_attribute" do
|
252
|
-
let (:model) { Artist.new }
|
253
|
-
let (:klass) do
|
254
|
-
require 'reform/active_record'
|
255
|
-
|
256
|
-
Class.new Reform::Form do
|
257
|
-
include Reform::Form::ActiveRecord
|
258
|
-
|
259
|
-
model :artist
|
260
|
-
|
261
|
-
property :name
|
262
|
-
end
|
263
|
-
end
|
264
|
-
let (:form) { klass.new(model) }
|
265
|
-
|
266
|
-
it 'should delegate to the model' do
|
267
|
-
Calls = []
|
268
|
-
|
269
|
-
def model.column_for_attribute(*args)
|
270
|
-
Calls << [:column_for_attribute, *args]
|
271
|
-
end
|
272
|
-
|
273
|
-
form.column_for_attribute(:name)
|
274
|
-
Calls.must_include [:column_for_attribute, :name]
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
describe "#column_for_attribute with composition" do
|
279
|
-
let (:artist_model) { Artist.new }
|
280
|
-
let (:song_model) { Song.new }
|
281
|
-
let (:form_klass) do
|
282
|
-
require 'reform/active_record'
|
283
|
-
|
284
|
-
Class.new Reform::Form do
|
285
|
-
include Reform::Form::ActiveRecord
|
286
|
-
include Reform::Form::Composition
|
287
|
-
|
288
|
-
model :artist
|
289
|
-
|
290
|
-
property :name, on: :artist
|
291
|
-
property :title, on: :song
|
292
|
-
end
|
293
|
-
end
|
294
|
-
let (:form) { form_klass.new(artist: artist_model, song: song_model) }
|
295
|
-
|
296
|
-
it 'should delegate to the model' do
|
297
|
-
ArtistCalls, SongCalls = [], []
|
298
|
-
|
299
|
-
def artist_model.column_for_attribute(*args)
|
300
|
-
ArtistCalls << [:column_for_attribute, *args]
|
301
|
-
end
|
302
|
-
|
303
|
-
def song_model.column_for_attribute(*args)
|
304
|
-
SongCalls << [:column_for_attribute, *args]
|
305
|
-
end
|
306
|
-
|
307
|
-
form.column_for_attribute(:name)
|
308
|
-
ArtistCalls.must_include [:column_for_attribute, :name]
|
309
|
-
|
310
|
-
form.column_for_attribute(:title)
|
311
|
-
SongCalls.must_include [:column_for_attribute, :title]
|
312
|
-
end
|
313
|
-
end
|
314
|
-
end
|
315
|
-
|
316
|
-
|
317
|
-
class EmptyAttributesTest < MiniTest::Spec
|
318
|
-
Credentials = Struct.new(:password)
|
319
|
-
|
320
|
-
class PasswordForm < Reform::Form
|
321
|
-
property :password
|
322
|
-
property :password_confirmation, :empty => true
|
323
|
-
end
|
324
|
-
|
325
|
-
let (:cred) { Credentials.new }
|
326
|
-
let (:form) { PasswordForm.new(cred) }
|
327
|
-
|
328
|
-
before { form.validate("password" => "123", "password_confirmation" => "321") }
|
329
|
-
|
330
|
-
it {
|
331
|
-
form.password.must_equal "123"
|
332
|
-
form.password_confirmation.must_equal "321"
|
333
|
-
|
334
|
-
form.sync
|
335
|
-
cred.password.must_equal "123"
|
336
|
-
|
337
|
-
hash = {}
|
338
|
-
form.save do |nested|
|
339
|
-
hash = nested
|
340
|
-
end
|
341
|
-
|
342
|
-
hash.must_equal("password"=> "123", "password_confirmation" => "321")
|
343
|
-
}
|
344
|
-
end
|
345
|
-
|
346
|
-
class ReadonlyAttributesTest < MiniTest::Spec
|
347
|
-
Location = Struct.new(:country)
|
348
|
-
|
349
|
-
class LocationForm < Reform::Form
|
350
|
-
property :country, :virtual => true # read_only: true
|
351
|
-
end
|
352
|
-
|
353
|
-
let (:loc) { Location.new("Australia") }
|
354
|
-
let (:form) { LocationForm.new(loc) }
|
355
|
-
|
356
|
-
it { form.country.must_equal "Australia" }
|
357
|
-
it do
|
358
|
-
form.validate("country" => "Germany") # this usually won't change when submitting.
|
359
|
-
form.country.must_equal "Germany"
|
360
|
-
|
361
|
-
|
362
|
-
form.sync
|
363
|
-
loc.country.must_equal "Australia" # the writer wasn't called.
|
364
|
-
|
365
|
-
hash = {}
|
366
|
-
form.save do |nested|
|
367
|
-
hash = nested
|
368
|
-
end
|
369
|
-
|
370
|
-
hash.must_equal("country"=> "Germany")
|
371
|
-
end
|
372
199
|
end
|
373
200
|
|
374
201
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RepresenterOptionsTest < MiniTest::Spec
|
4
|
+
subject { Reform::Representer::Options[] }
|
5
|
+
|
6
|
+
# don't maintain empty excludes until fixed in representable.
|
7
|
+
it { subject.exclude!([]).must_equal({:exclude=>[]}) }
|
8
|
+
it { subject.include!([]).must_equal({:include=>[]}) }
|
9
|
+
|
10
|
+
it { subject.exclude!([:title, :id]).must_equal({exclude: [:title, :id]}) }
|
11
|
+
it { subject.include!([:title, :id]).must_equal({include: [:title, :id]}) }
|
12
|
+
|
13
|
+
|
14
|
+
module Representer
|
15
|
+
include Representable::Hash
|
16
|
+
property :title
|
17
|
+
property :genre
|
18
|
+
property :id
|
19
|
+
end
|
20
|
+
|
21
|
+
it "representable" do
|
22
|
+
song = OpenStruct.new(title: "Title", genre: "Punk", id: 1)
|
23
|
+
puts Representer.prepare(song).to_hash(include: [:genre, :id], exclude: [:id]).inspect
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
class RepresenterTest < MiniTest::Spec
|
29
|
+
class SongRepresenter < Reform::Representer
|
30
|
+
property :title
|
31
|
+
property :name
|
32
|
+
property :genre
|
33
|
+
end
|
34
|
+
|
35
|
+
subject { SongRepresenter.new(Object.new) }
|
36
|
+
|
37
|
+
describe "#fields" do
|
38
|
+
it "returns all properties as strings" do
|
39
|
+
SongRepresenter.fields.must_equal(["title", "name", "genre"])
|
40
|
+
end
|
41
|
+
|
42
|
+
# allows block.
|
43
|
+
it do
|
44
|
+
SongRepresenter.fields { |dfn| dfn.name =~ /n/ }.must_equal ["name", "genre"]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/test/save_test.rb
CHANGED
@@ -1,6 +1,30 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class SaveTest < BaseTest
|
4
|
+
class AlbumForm < Reform::Form
|
5
|
+
property :title
|
6
|
+
|
7
|
+
property :hit do
|
8
|
+
property :title
|
9
|
+
validates :title, :presence => true
|
10
|
+
end
|
11
|
+
|
12
|
+
collection :songs do
|
13
|
+
property :title
|
14
|
+
validates :title, :presence => true
|
15
|
+
end
|
16
|
+
|
17
|
+
property :band do # yepp, people do crazy stuff like that.
|
18
|
+
property :label do
|
19
|
+
property :name
|
20
|
+
validates :name, :presence => true
|
21
|
+
end
|
22
|
+
# TODO: make band a required object.
|
23
|
+
end
|
24
|
+
|
25
|
+
validates :title, :presence => true
|
26
|
+
end
|
27
|
+
|
4
28
|
let (:params) {
|
5
29
|
{
|
6
30
|
"title" => "Best Of",
|
@@ -17,7 +41,7 @@ class SaveTest < BaseTest
|
|
17
41
|
let (:band) { Band.new(label) }
|
18
42
|
let (:label) { Label.new }
|
19
43
|
|
20
|
-
subject {
|
44
|
+
subject { AlbumForm.new(album) }
|
21
45
|
|
22
46
|
before do
|
23
47
|
[album, hit, song1, song2, band, label].each { |mdl| mdl.extend(Saveable) }
|
@@ -86,4 +110,30 @@ class SaveTest < BaseTest
|
|
86
110
|
album.instance_eval { def save; false; end }
|
87
111
|
subject.save.must_equal false
|
88
112
|
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
class SaveWithDynamicOptionsTest < MiniTest::Spec
|
117
|
+
Song = Struct.new(:id, :title, :length) do
|
118
|
+
include Saveable
|
119
|
+
end
|
120
|
+
|
121
|
+
class SongForm < Reform::Form
|
122
|
+
property :title#, save: false
|
123
|
+
property :length, virtual: true
|
124
|
+
end
|
125
|
+
|
126
|
+
let (:song) { Song.new }
|
127
|
+
let (:form) { SongForm.new(song) }
|
128
|
+
|
129
|
+
# we have access to original input value and outside parameters.
|
130
|
+
it "xxx" do
|
131
|
+
form.validate("title" => "A Poor Man's Memory", "length" => 10)
|
132
|
+
length_seconds = 120
|
133
|
+
form.save(length: lambda { |value, options| form.model.id = "#{value}: #{length_seconds}" })
|
134
|
+
|
135
|
+
song.title.must_equal "A Poor Man's Memory"
|
136
|
+
song.length.must_equal nil
|
137
|
+
song.id.must_equal "10: 120"
|
138
|
+
end
|
89
139
|
end
|
data/test/scalar_test.rb
CHANGED
@@ -163,23 +163,5 @@ class SelfNestedTest < BaseTest
|
|
163
163
|
form.validate({}).must_equal true
|
164
164
|
end
|
165
165
|
|
166
|
-
|
167
|
-
it do
|
168
|
-
form = StringForm.new(AlbumCover.new(nil))
|
169
|
-
form.validate({"image"=>""}).must_equal true
|
170
|
-
end
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
# TODO: move to validate_test.
|
175
|
-
# property :rejection_reason, scalar: true, virtual: true, empty: true do # optional parameter
|
176
|
-
# validates length: {minimum: 5}, if: lambda { model.present? and avatar_moderation == "2" } # IF present, at least 5 characters.
|
177
|
-
# end
|
178
|
-
class BlaForm < Reform::Form
|
179
|
-
property :image# creates "empty" form
|
180
|
-
validates :image, :length => {:minimum => 10}, if: lambda { image and image != "" }
|
181
|
-
end
|
182
|
-
|
183
|
-
it { BlaForm.new(AlbumCover.new(nil)).validate({"image"=>""}).must_equal true }
|
184
166
|
# DISCUSS: when AlbumCover.new("Hello").validate({}), does that fail?
|
185
167
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SkipIfTest < BaseTest
|
4
|
+
|
5
|
+
class AlbumForm < Reform::Form
|
6
|
+
property :title
|
7
|
+
|
8
|
+
property :hit, skip_if: lambda { |fragment, *| fragment["title"].blank? } do
|
9
|
+
property :title
|
10
|
+
validates :title, presence: true
|
11
|
+
end
|
12
|
+
|
13
|
+
collection :songs, skip_if: lambda { |fragment, *| fragment["title"].nil? },
|
14
|
+
populate_if_empty: BaseTest::Song do
|
15
|
+
property :title
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
let (:hit) { Song.new }
|
21
|
+
let (:album) { Album.new(nil, hit, [], nil) }
|
22
|
+
|
23
|
+
# deserializes when present.
|
24
|
+
it do
|
25
|
+
form = AlbumForm.new(album)
|
26
|
+
form.validate("hit" => {"title" => "Altar Of Sacrifice"}).must_equal true
|
27
|
+
form.hit.title.must_equal "Altar Of Sacrifice"
|
28
|
+
end
|
29
|
+
|
30
|
+
# skips deserialization when not present.
|
31
|
+
it do
|
32
|
+
form = AlbumForm.new(Album.new)
|
33
|
+
form.validate("hit" => {"title" => ""}).must_equal true
|
34
|
+
form.hit.must_equal nil # hit hasn't been deserialised.
|
35
|
+
end
|
36
|
+
|
37
|
+
# skips deserialization when not present.
|
38
|
+
it do
|
39
|
+
form = AlbumForm.new(Album.new(nil, nil, []))
|
40
|
+
form.validate("songs" => [{"title" => "Waste Of Breath"}, {"title" => nil}]).must_equal true
|
41
|
+
form.songs.size.must_equal 1
|
42
|
+
form.songs[0].title.must_equal "Waste Of Breath"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class SkipIfAllBlankTest < BaseTest
|
47
|
+
# skip_if: :all_blank"
|
48
|
+
class AlbumForm < Reform::Form
|
49
|
+
collection :songs, skip_if: :all_blank, populate_if_empty: BaseTest::Song do
|
50
|
+
property :title
|
51
|
+
property :length
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# create only one object.
|
56
|
+
it do
|
57
|
+
form = AlbumForm.new(OpenStruct.new(songs: []))
|
58
|
+
form.validate("songs" => [{"title"=>"Apathy"}, {"title"=>"", "length" => ""}]).must_equal true
|
59
|
+
form.songs.size.must_equal 1
|
60
|
+
form.songs[0].title.must_equal "Apathy"
|
61
|
+
end
|
62
|
+
end
|