reform 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +13 -0
- data/Gemfile +1 -1
- data/README.md +61 -13
- data/database.sqlite3 +0 -0
- data/lib/reform/composition.rb +1 -0
- data/lib/reform/contract.rb +24 -15
- data/lib/reform/form.rb +5 -0
- data/lib/reform/form/active_model.rb +5 -3
- data/lib/reform/form/active_model/model_validations.rb +98 -0
- data/lib/reform/form/active_record.rb +8 -1
- data/lib/reform/form/coercion.rb +4 -4
- data/lib/reform/form/composition.rb +2 -19
- data/lib/reform/form/module.rb +27 -0
- data/lib/reform/form/multi_parameter_attributes.rb +22 -2
- data/lib/reform/form/save.rb +18 -5
- data/lib/reform/form/scalar.rb +52 -0
- data/lib/reform/form/sync.rb +5 -6
- data/lib/reform/form/validate.rb +40 -57
- data/lib/reform/rails.rb +3 -3
- data/lib/reform/representer.rb +14 -7
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +6 -4
- data/test/active_model_test.rb +4 -23
- data/test/active_record_test.rb +188 -42
- data/test/as_test.rb +1 -1
- data/test/builder_test.rb +32 -0
- data/test/coercion_test.rb +56 -28
- data/test/deserialize_test.rb +40 -0
- data/test/form_builder_test.rb +1 -1
- data/test/form_composition_test.rb +50 -22
- data/test/inherit_test.rb +79 -0
- data/test/model_validations_test.rb +82 -0
- data/test/nested_form_test.rb +2 -13
- data/test/reform_test.rb +106 -7
- data/test/scalar_test.rb +167 -0
- data/test/test_helper.rb +10 -0
- data/test/validate_test.rb +42 -2
- metadata +37 -12
- data/test/setup_test.rb +0 -68
@@ -1,28 +1,32 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class FormCompositionTest < MiniTest::Spec
|
4
|
-
Song = Struct.new(:id, :title)
|
4
|
+
Song = Struct.new(:id, :title, :band)
|
5
5
|
Requester = Struct.new(:id, :name, :requester)
|
6
|
+
Band = Struct.new(:title)
|
6
7
|
|
7
8
|
class RequestForm < Reform::Form
|
8
9
|
include Composition
|
9
10
|
|
10
|
-
|
11
|
-
property :
|
12
|
-
property :requester_id, :on => :requester, :as => :id, :skip_accessors => true
|
11
|
+
property :name, :on => :requester
|
12
|
+
property :requester_id, :on => :requester, :as => :id
|
13
13
|
properties [:title, :id], :on => :song
|
14
14
|
# property :channel # FIXME: what about the "main model"?
|
15
15
|
property :channel, :empty => true, :on => :song
|
16
|
-
property :requester, :on => :requester
|
16
|
+
property :requester, :on => :requester
|
17
17
|
property :captcha, :on => :song, :empty => true
|
18
18
|
|
19
19
|
validates :name, :title, :channel, :presence => true
|
20
|
-
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
property :band, :on => :song do
|
22
|
+
property :title
|
23
|
+
end
|
24
|
+
end
|
25
25
|
|
26
|
+
let (:form) { RequestForm.new(:song => song, :requester => requester) }
|
27
|
+
let (:song) { Song.new(1, "Rio", band) }
|
28
|
+
let (:requester) { Requester.new(2, "Duran Duran", "MCP") }
|
29
|
+
let (:band) { Band.new("Duran^2") }
|
26
30
|
|
27
31
|
# delegation form -> composition works
|
28
32
|
it { form.id.must_equal 1 }
|
@@ -33,11 +37,6 @@ class FormCompositionTest < MiniTest::Spec
|
|
33
37
|
it { form.requester.must_equal "MCP" } # same name as composed model.
|
34
38
|
it { form.captcha.must_equal nil }
|
35
39
|
|
36
|
-
# [DEPRECATED] # TODO: remove in 1.2.
|
37
|
-
# delegation form -> composed models (e.g. when saving this can be handy)
|
38
|
-
it { form.song.must_equal song }
|
39
|
-
|
40
|
-
|
41
40
|
# #model just returns <Composition>.
|
42
41
|
it { form.model.must_be_kind_of Reform::Composition }
|
43
42
|
|
@@ -51,12 +50,13 @@ class FormCompositionTest < MiniTest::Spec
|
|
51
50
|
end
|
52
51
|
|
53
52
|
describe "#save" do
|
54
|
-
|
53
|
+
# #save with {}
|
54
|
+
it do
|
55
55
|
hash = {}
|
56
56
|
|
57
|
-
form.save do |
|
58
|
-
hash[:name] =
|
59
|
-
hash[:title] =
|
57
|
+
form.save do |map|
|
58
|
+
hash[:name] = form.name
|
59
|
+
hash[:title] = form.title
|
60
60
|
end
|
61
61
|
|
62
62
|
hash.must_equal({:name=>"Duran Duran", :title=>"Rio"})
|
@@ -65,21 +65,23 @@ class FormCompositionTest < MiniTest::Spec
|
|
65
65
|
it "provides nested symbolized hash as second block argument" do
|
66
66
|
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "channel" => "JJJ", "captcha" => "wonderful")
|
67
67
|
|
68
|
-
hash =
|
68
|
+
hash = nil
|
69
69
|
|
70
|
-
form.save do |
|
70
|
+
form.save do |map|
|
71
71
|
hash = map
|
72
72
|
end
|
73
73
|
|
74
74
|
hash.must_equal({
|
75
|
-
:song=>{:title=>"Greyhound", :id=>1, :channel => "JJJ", :captcha=>"wonderful"},
|
76
|
-
:requester=>{:name=>"Frenzal Rhomb", :id=>2, :requester => "MCP"}
|
75
|
+
:song=>{:title=>"Greyhound", :id=>1, :channel => "JJJ", :captcha=>"wonderful", :band=>{"title"=>"Duran^2"}},
|
76
|
+
:requester=>{:name=>"Frenzal Rhomb", :id=>2, :requester => "MCP"}
|
77
|
+
}
|
77
78
|
)
|
78
79
|
end
|
79
80
|
|
80
81
|
it "pushes data to models and calls #save when no block passed" do
|
81
82
|
song.extend(Saveable)
|
82
83
|
requester.extend(Saveable)
|
84
|
+
band.extend(Saveable)
|
83
85
|
|
84
86
|
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
|
85
87
|
form.captcha.must_equal "1337" # TODO: move to separate test.
|
@@ -90,6 +92,32 @@ class FormCompositionTest < MiniTest::Spec
|
|
90
92
|
requester.saved?.must_equal true
|
91
93
|
song.title.must_equal "Greyhound"
|
92
94
|
song.saved?.must_equal true
|
95
|
+
song.band.title.must_equal "Duran^2"
|
96
|
+
song.band.saved?.must_equal true
|
93
97
|
end
|
94
98
|
end
|
95
99
|
end
|
100
|
+
|
101
|
+
|
102
|
+
class FormCompositionCollectionTest < MiniTest::Spec
|
103
|
+
Book = Struct.new(:id, :name)
|
104
|
+
Library = Struct.new(:id) do
|
105
|
+
def books
|
106
|
+
[Book.new(1,"My book")]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class LibraryForm < Reform::Form
|
111
|
+
include Reform::Form::Composition
|
112
|
+
|
113
|
+
collection :books, on: :library do
|
114
|
+
property :id
|
115
|
+
property :name
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
let (:form) { LibraryForm.new(library: library) }
|
120
|
+
let (:library) { Library.new(2) }
|
121
|
+
|
122
|
+
it { form.save do |hash| hash.must_equal({:library=>{:books=>[{"id"=>1, "name"=>"My book"}]}}) end }
|
123
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'representable/json'
|
3
|
+
|
4
|
+
class InheritTest < BaseTest
|
5
|
+
class CompilationForm < AlbumForm
|
6
|
+
|
7
|
+
property :hit, :inherit => true do
|
8
|
+
property :rating
|
9
|
+
validates :title, :rating, :presence => true
|
10
|
+
end
|
11
|
+
|
12
|
+
# puts representer_class.representable_attrs.
|
13
|
+
# get(:hit)[:extend].evaluate(nil).new(OpenStruct.new).rating
|
14
|
+
end
|
15
|
+
|
16
|
+
let (:album) { Album.new(nil, OpenStruct.new(:hit => OpenStruct.new()) ) }
|
17
|
+
subject { CompilationForm.new(album) }
|
18
|
+
|
19
|
+
|
20
|
+
# valid.
|
21
|
+
it {
|
22
|
+
subject.validate("hit" => {"title" => "LA Drone", "rating" => 10})
|
23
|
+
subject.hit.title.must_equal "LA Drone"
|
24
|
+
subject.hit.rating.must_equal 10
|
25
|
+
subject.errors.messages.must_equal({})
|
26
|
+
}
|
27
|
+
|
28
|
+
it do
|
29
|
+
subject.validate({})
|
30
|
+
subject.hit.title.must_equal nil
|
31
|
+
subject.hit.rating.must_equal nil
|
32
|
+
subject.errors.messages.must_equal({:"hit.title"=>["can't be blank"], :"hit.rating"=>["can't be blank"]})
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class ModuleInclusionTest < MiniTest::Spec
|
38
|
+
module BandPropertyForm
|
39
|
+
include Reform::Form::Module
|
40
|
+
|
41
|
+
property :band do
|
42
|
+
property :title
|
43
|
+
|
44
|
+
validates :title, :presence => true
|
45
|
+
|
46
|
+
def id # gets mixed into Form, too.
|
47
|
+
2
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def id # gets mixed into Form, too.
|
52
|
+
1
|
53
|
+
end
|
54
|
+
|
55
|
+
validates :band, :presence => true
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class SongForm < Reform::Form
|
60
|
+
property :title
|
61
|
+
|
62
|
+
include BandPropertyForm
|
63
|
+
end
|
64
|
+
|
65
|
+
let (:song) { OpenStruct.new(:band => OpenStruct.new(:title => "Time Again")) }
|
66
|
+
|
67
|
+
# nested form from module is present and creates accessor.
|
68
|
+
it { SongForm.new(song).band.title.must_equal "Time Again" }
|
69
|
+
|
70
|
+
# methods from module get included.
|
71
|
+
it { SongForm.new(song).id.must_equal 1 }
|
72
|
+
it { SongForm.new(song).band.id.must_equal 2 }
|
73
|
+
|
74
|
+
it do
|
75
|
+
form = SongForm.new(OpenStruct.new())
|
76
|
+
form.validate({})
|
77
|
+
form.errors.messages.must_equal({:band=>["can't be blank"]})
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ModelValidationsTest < MiniTest::Spec
|
4
|
+
|
5
|
+
class Album
|
6
|
+
include ActiveModel::Validations
|
7
|
+
attr_accessor :title, :artist, :other_attribute
|
8
|
+
|
9
|
+
validates :title, :artist, presence: true
|
10
|
+
validates :other_attribute, presence: true
|
11
|
+
end
|
12
|
+
|
13
|
+
class AlbumRating
|
14
|
+
include ActiveModel::Validations
|
15
|
+
|
16
|
+
attr_accessor :rating
|
17
|
+
|
18
|
+
validates :rating, numericality: { greater_than_or_equal_to: 0 }
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class AlbumForm < Reform::Form
|
23
|
+
extend ActiveModel::ModelValidations
|
24
|
+
|
25
|
+
property :title
|
26
|
+
property :artist_name, as: :artist
|
27
|
+
copy_validations_from Album
|
28
|
+
end
|
29
|
+
|
30
|
+
class CompositeForm < Reform::Form
|
31
|
+
include Composition
|
32
|
+
extend ActiveModel::ModelValidations
|
33
|
+
|
34
|
+
model :album
|
35
|
+
|
36
|
+
property :title, on: :album
|
37
|
+
property :artist_name, as: :artist, on: :album
|
38
|
+
property :rating, on: :album_rating
|
39
|
+
|
40
|
+
copy_validations_from album: Album, album_rating: AlbumRating
|
41
|
+
end
|
42
|
+
|
43
|
+
let(:album) { Album.new }
|
44
|
+
|
45
|
+
describe 'non-composite form' do
|
46
|
+
|
47
|
+
let(:album_form) { AlbumForm.new(album) }
|
48
|
+
|
49
|
+
it 'is not valid when title is not present' do
|
50
|
+
album_form.validate(artist_name: 'test', title: nil).must_equal false
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'is not valid when artist_name is not present' do
|
54
|
+
album_form.validate(artist_name: nil, title: 'test').must_equal false
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'is valid when title and artist_name is present' do
|
58
|
+
album_form.validate(artist_name: 'test', title: 'test').must_equal true
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'composite form' do
|
64
|
+
|
65
|
+
let(:album_rating) { AlbumRating.new }
|
66
|
+
let(:composite_form) { CompositeForm.new(album: album, album_rating: album_rating) }
|
67
|
+
|
68
|
+
it 'is valid when all attributes are correct' do
|
69
|
+
composite_form.validate(artist_name: 'test', title: 'test', rating: 1).must_equal true
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'is invalid when rating is below 0' do
|
73
|
+
composite_form.validate(artist_name: 'test', title: 'test', rating: -1).must_equal false
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'is invalid when artist_name is missing' do
|
77
|
+
composite_form.validate(artist_name: nil, title: 'test', rating: 1).must_equal false
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
data/test/nested_form_test.rb
CHANGED
@@ -39,7 +39,7 @@ class NestedFormTest < MiniTest::Spec
|
|
39
39
|
|
40
40
|
it "responds to #save" do
|
41
41
|
hsh = nil
|
42
|
-
form.save do |
|
42
|
+
form.save do |nested|
|
43
43
|
hsh = nested
|
44
44
|
end
|
45
45
|
hsh.must_equal({"hit"=>{"title"=>"Downtown"}, "title" => "Blackhawks Over Los Angeles", "songs"=>[{"title"=>"Calling"}]})
|
@@ -103,21 +103,10 @@ class NestedFormTest < MiniTest::Spec
|
|
103
103
|
data.must_equal(:title=>"Second Heat", :hit_title => "Sacrifice", :first_title => "Scarified")
|
104
104
|
end
|
105
105
|
|
106
|
-
it "passes form instances in first argument" do
|
107
|
-
frm = nil
|
108
|
-
|
109
|
-
form.save { |f, hsh| frm = f }
|
110
|
-
|
111
|
-
frm.must_equal form
|
112
|
-
frm.title.must_be_kind_of String
|
113
|
-
frm.hit.must_be_kind_of Reform::Form
|
114
|
-
frm.songs.first.must_be_kind_of Reform::Form
|
115
|
-
end
|
116
|
-
|
117
106
|
it "returns nested hash with indifferent access" do
|
118
107
|
nested = nil
|
119
108
|
|
120
|
-
form.save do |
|
109
|
+
form.save do |nested_hash|
|
121
110
|
nested = nested_hash
|
122
111
|
end
|
123
112
|
|
data/test/reform_test.rb
CHANGED
@@ -204,7 +204,7 @@ class ReformTest < ReformSpec
|
|
204
204
|
end
|
205
205
|
|
206
206
|
describe "#save with block" do
|
207
|
-
it "provides data block argument" do
|
207
|
+
it "Deprecated: provides data block argument" do # TODO: remove in 1.1.
|
208
208
|
hash = {}
|
209
209
|
|
210
210
|
form.save do |data, map|
|
@@ -215,7 +215,7 @@ class ReformTest < ReformSpec
|
|
215
215
|
hash.must_equal({:name=>"Diesel Boy", :title=>nil})
|
216
216
|
end
|
217
217
|
|
218
|
-
it "provides nested symbolized hash as second block argument" do
|
218
|
+
it "Deprecated: provides nested symbolized hash as second block argument" do # TODO: remove in 1.1.
|
219
219
|
hash = {}
|
220
220
|
|
221
221
|
form.save do |data, map|
|
@@ -224,6 +224,16 @@ class ReformTest < ReformSpec
|
|
224
224
|
|
225
225
|
hash.must_equal({"name"=>"Diesel Boy"})
|
226
226
|
end
|
227
|
+
|
228
|
+
it do
|
229
|
+
hash = {}
|
230
|
+
|
231
|
+
form.save do |map|
|
232
|
+
hash = map
|
233
|
+
end
|
234
|
+
|
235
|
+
hash.must_equal({"name"=>"Diesel Boy"})
|
236
|
+
end
|
227
237
|
end
|
228
238
|
end
|
229
239
|
|
@@ -239,7 +249,7 @@ class ReformTest < ReformSpec
|
|
239
249
|
validates :position, :presence => true
|
240
250
|
end
|
241
251
|
|
242
|
-
let (:form) { HitForm.new(OpenStruct.new) }
|
252
|
+
let (:form) { HitForm.new(OpenStruct.new()) }
|
243
253
|
it do
|
244
254
|
form.validate({"title" => "The Body"})
|
245
255
|
form.title.must_equal "The Body"
|
@@ -247,8 +257,73 @@ class ReformTest < ReformSpec
|
|
247
257
|
form.errors.messages.must_equal({:name=>["can't be blank"], :position=>["can't be blank"]})
|
248
258
|
end
|
249
259
|
end
|
260
|
+
|
261
|
+
describe "#column_for_attribute" do
|
262
|
+
let (:model) { Artist.new }
|
263
|
+
let (:klass) do
|
264
|
+
require 'reform/active_record'
|
265
|
+
|
266
|
+
Class.new Reform::Form do
|
267
|
+
include Reform::Form::ActiveRecord
|
268
|
+
|
269
|
+
model :artist
|
270
|
+
|
271
|
+
property :name
|
272
|
+
end
|
273
|
+
end
|
274
|
+
let (:form) { klass.new(model) }
|
275
|
+
|
276
|
+
it 'should delegate to the model' do
|
277
|
+
Calls = []
|
278
|
+
|
279
|
+
def model.column_for_attribute(*args)
|
280
|
+
Calls << [:column_for_attribute, *args]
|
281
|
+
end
|
282
|
+
|
283
|
+
form.column_for_attribute(:name)
|
284
|
+
Calls.must_include [:column_for_attribute, :name]
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
describe "#column_for_attribute with composition" do
|
289
|
+
let (:artist_model) { Artist.new }
|
290
|
+
let (:song_model) { Song.new }
|
291
|
+
let (:form_klass) do
|
292
|
+
require 'reform/active_record'
|
293
|
+
|
294
|
+
Class.new Reform::Form do
|
295
|
+
include Reform::Form::ActiveRecord
|
296
|
+
include Reform::Form::Composition
|
297
|
+
|
298
|
+
model :artist
|
299
|
+
|
300
|
+
property :name, on: :artist
|
301
|
+
property :title, on: :song
|
302
|
+
end
|
303
|
+
end
|
304
|
+
let (:form) { form_klass.new(artist: artist_model, song: song_model) }
|
305
|
+
|
306
|
+
it 'should delegate to the model' do
|
307
|
+
ArtistCalls, SongCalls = [], []
|
308
|
+
|
309
|
+
def artist_model.column_for_attribute(*args)
|
310
|
+
ArtistCalls << [:column_for_attribute, *args]
|
311
|
+
end
|
312
|
+
|
313
|
+
def song_model.column_for_attribute(*args)
|
314
|
+
SongCalls << [:column_for_attribute, *args]
|
315
|
+
end
|
316
|
+
|
317
|
+
form.column_for_attribute(:name)
|
318
|
+
ArtistCalls.must_include [:column_for_attribute, :name]
|
319
|
+
|
320
|
+
form.column_for_attribute(:title)
|
321
|
+
SongCalls.must_include [:column_for_attribute, :title]
|
322
|
+
end
|
323
|
+
end
|
250
324
|
end
|
251
325
|
|
326
|
+
|
252
327
|
class EmptyAttributesTest < MiniTest::Spec
|
253
328
|
Credentials = Struct.new(:password)
|
254
329
|
|
@@ -270,7 +345,7 @@ class EmptyAttributesTest < MiniTest::Spec
|
|
270
345
|
cred.password.must_equal "123"
|
271
346
|
|
272
347
|
hash = {}
|
273
|
-
form.save do |
|
348
|
+
form.save do |nested|
|
274
349
|
hash = nested
|
275
350
|
end
|
276
351
|
|
@@ -298,7 +373,7 @@ class ReadonlyAttributesTest < MiniTest::Spec
|
|
298
373
|
loc.country.must_equal "Australia" # the writer wasn't called.
|
299
374
|
|
300
375
|
hash = {}
|
301
|
-
form.save do |
|
376
|
+
form.save do |nested|
|
302
377
|
hash = nested
|
303
378
|
end
|
304
379
|
|
@@ -335,7 +410,7 @@ class OverridingAccessorsTest < BaseTest
|
|
335
410
|
|
336
411
|
# the reader is not used when saving/syncing.
|
337
412
|
it do
|
338
|
-
subject.save do |
|
413
|
+
subject.save do |hash|
|
339
414
|
hash["title"].must_equal "Hey Little WorldHey Little World"
|
340
415
|
end
|
341
416
|
end
|
@@ -347,4 +422,28 @@ class OverridingAccessorsTest < BaseTest
|
|
347
422
|
song.title.must_equal "Hey Little WorldHey Little World"
|
348
423
|
end
|
349
424
|
end
|
350
|
-
end
|
425
|
+
end
|
426
|
+
|
427
|
+
|
428
|
+
class MethodInFormTest < MiniTest::Spec
|
429
|
+
class AlbumForm < Reform::Form
|
430
|
+
property :title
|
431
|
+
|
432
|
+
def title
|
433
|
+
"The Suffer And The Witness"
|
434
|
+
end
|
435
|
+
|
436
|
+
property :hit do
|
437
|
+
property :title
|
438
|
+
|
439
|
+
def title
|
440
|
+
"Drones"
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# methods can be used instead of created accessors.
|
446
|
+
subject { AlbumForm.new(OpenStruct.new(:hit => OpenStruct.new)) }
|
447
|
+
it { subject.title.must_equal "The Suffer And The Witness" }
|
448
|
+
it { subject.hit.title.must_equal "Drones" }
|
449
|
+
end
|