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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +35 -1
  3. data/Gemfile +1 -1
  4. data/README.md +83 -21
  5. data/TODO.md +8 -0
  6. data/database.sqlite3 +0 -0
  7. data/gemfiles/Gemfile.rails-4.0 +1 -0
  8. data/lib/reform.rb +4 -2
  9. data/lib/reform/active_record.rb +2 -1
  10. data/lib/reform/composition.rb +2 -2
  11. data/lib/reform/contract.rb +24 -7
  12. data/lib/reform/contract/setup.rb +21 -9
  13. data/lib/reform/contract/validate.rb +0 -6
  14. data/lib/reform/form.rb +6 -8
  15. data/lib/reform/form/active_model.rb +3 -2
  16. data/lib/reform/form/active_model/model_validations.rb +13 -1
  17. data/lib/reform/form/active_record.rb +1 -7
  18. data/lib/reform/form/changed.rb +9 -0
  19. data/lib/reform/form/json.rb +13 -0
  20. data/lib/reform/form/model_reflections.rb +18 -0
  21. data/lib/reform/form/save.rb +25 -3
  22. data/lib/reform/form/scalar.rb +4 -2
  23. data/lib/reform/form/sync.rb +82 -12
  24. data/lib/reform/form/validate.rb +38 -0
  25. data/lib/reform/rails.rb +1 -1
  26. data/lib/reform/representer.rb +14 -23
  27. data/lib/reform/schema.rb +23 -0
  28. data/lib/reform/twin.rb +20 -0
  29. data/lib/reform/version.rb +1 -1
  30. data/reform.gemspec +2 -2
  31. data/test/active_model_test.rb +2 -2
  32. data/test/active_record_test.rb +7 -4
  33. data/test/changed_test.rb +69 -0
  34. data/test/custom_validation_test.rb +47 -0
  35. data/test/deserialize_test.rb +2 -7
  36. data/test/empty_test.rb +30 -0
  37. data/test/fields_test.rb +24 -0
  38. data/test/form_composition_test.rb +24 -2
  39. data/test/form_test.rb +84 -0
  40. data/test/inherit_test.rb +12 -0
  41. data/test/model_reflections_test.rb +65 -0
  42. data/test/read_only_test.rb +28 -0
  43. data/test/reform_test.rb +2 -175
  44. data/test/representer_test.rb +47 -0
  45. data/test/save_test.rb +51 -1
  46. data/test/scalar_test.rb +0 -18
  47. data/test/skip_if_test.rb +62 -0
  48. data/test/skip_unchanged_test.rb +86 -0
  49. data/test/sync_option_test.rb +83 -0
  50. data/test/twin_test.rb +23 -0
  51. data/test/validate_test.rb +9 -1
  52. metadata +37 -9
  53. 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 [:name, :title], opts
78
- properties [:created_at]
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 { ErrorsTest::AlbumForm.new(album) }
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