reform 1.0.4 → 1.1.0
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 +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
|