reform 1.1.1 → 1.2.0.beta1
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 +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
|