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.
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