reform 1.2.6 → 2.0.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -1
  3. data/CHANGES.md +14 -0
  4. data/Gemfile +3 -2
  5. data/README.md +225 -283
  6. data/Rakefile +27 -0
  7. data/TODO.md +12 -0
  8. data/database.sqlite3 +0 -0
  9. data/gemfiles/Gemfile.rails-3.0 +1 -0
  10. data/gemfiles/Gemfile.rails-3.1 +1 -0
  11. data/gemfiles/Gemfile.rails-3.2 +1 -0
  12. data/gemfiles/Gemfile.rails-4.0 +1 -0
  13. data/lib/reform.rb +0 -1
  14. data/lib/reform/contract.rb +64 -170
  15. data/lib/reform/contract/validate.rb +10 -13
  16. data/lib/reform/form.rb +74 -19
  17. data/lib/reform/form/active_model.rb +19 -14
  18. data/lib/reform/form/coercion.rb +1 -13
  19. data/lib/reform/form/composition.rb +2 -24
  20. data/lib/reform/form/multi_parameter_attributes.rb +43 -62
  21. data/lib/reform/form/populator.rb +85 -0
  22. data/lib/reform/form/prepopulate.rb +13 -43
  23. data/lib/reform/form/validate.rb +29 -90
  24. data/lib/reform/form/validation/unique_validator.rb +13 -0
  25. data/lib/reform/version.rb +1 -1
  26. data/reform.gemspec +7 -7
  27. data/test/active_model_test.rb +43 -0
  28. data/test/changed_test.rb +23 -51
  29. data/test/coercion_test.rb +1 -7
  30. data/test/composition_test.rb +128 -34
  31. data/test/contract_test.rb +27 -86
  32. data/test/feature_test.rb +43 -6
  33. data/test/fields_test.rb +2 -12
  34. data/test/form_builder_test.rb +28 -25
  35. data/test/form_option_test.rb +19 -0
  36. data/test/from_test.rb +0 -75
  37. data/test/inherit_test.rb +178 -117
  38. data/test/model_reflections_test.rb +1 -1
  39. data/test/populate_test.rb +226 -0
  40. data/test/prepopulator_test.rb +112 -0
  41. data/test/readable_test.rb +2 -4
  42. data/test/save_test.rb +56 -112
  43. data/test/setup_test.rb +48 -0
  44. data/test/skip_if_test.rb +5 -2
  45. data/test/skip_setter_and_getter_test.rb +54 -0
  46. data/test/test_helper.rb +3 -1
  47. data/test/uniqueness_test.rb +41 -0
  48. data/test/validate_test.rb +325 -289
  49. data/test/virtual_test.rb +1 -3
  50. data/test/writeable_test.rb +3 -4
  51. metadata +35 -39
  52. data/lib/reform/composition.rb +0 -63
  53. data/lib/reform/contract/setup.rb +0 -50
  54. data/lib/reform/form/changed.rb +0 -9
  55. data/lib/reform/form/sync.rb +0 -116
  56. data/lib/reform/representer.rb +0 -84
  57. data/test/empty_test.rb +0 -58
  58. data/test/form_composition_test.rb +0 -145
  59. data/test/nested_form_test.rb +0 -197
  60. data/test/prepopulate_test.rb +0 -85
  61. data/test/sync_option_test.rb +0 -83
  62. data/test/sync_test.rb +0 -56
@@ -85,7 +85,7 @@ class ModelReflectionTest < MiniTest::Spec
85
85
  # delegate to model class.
86
86
  it do
87
87
  reflection = form.class.reflect_on_association(:artist)
88
- reflection.must_be_instance_of ActiveRecord::Reflection::AssociationReflection
88
+ reflection.must_be_kind_of ActiveRecord::Reflection::AssociationReflection
89
89
  end
90
90
  end
91
91
 
@@ -0,0 +1,226 @@
1
+ require "test_helper"
2
+
3
+ class PopulatorTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ class AlbumForm < Reform::Form
9
+ property :name
10
+ validates :name, presence: true
11
+
12
+ collection :songs,
13
+ populator: lambda { |fragment, collection, index, options|
14
+ # collection = options.binding.get # we don't need this anymore as this comes in for free!
15
+ (item = collection[index]) ? item : collection.insert(index, Song.new) } do
16
+
17
+ property :title
18
+ validates :title, presence: true
19
+
20
+ property :composer, populator: lambda { |fragment, model, options| model || self.composer= Artist.new } do
21
+ property :name
22
+ validates :name, presence: true
23
+ end
24
+ end
25
+
26
+ # property :artist, populator: lambda { |fragment, options| (item = options.binding.get) ? item : Artist.new } do
27
+ # NOTE: we have to document that model here is the twin!
28
+ property :artist, populator: lambda { |fragment, twin, *| twin || self.artist = Artist.new } do
29
+ property :name
30
+ end
31
+ end
32
+
33
+ let (:song) { Song.new("Broken") }
34
+ let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
35
+ let (:composer) { Artist.new("Greg Graffin") }
36
+ let (:artist) { Artist.new("Bad Religion") }
37
+ let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
38
+
39
+ let (:form) { AlbumForm.new(album) }
40
+
41
+ # changing existing property :artist.
42
+ # TODO: check with artist==nil
43
+ it do
44
+ old_id = artist.object_id
45
+
46
+ form.validate(
47
+ "artist" => {"name" => "Marcus Miller"}
48
+ )
49
+
50
+ form.artist.model.object_id.must_equal old_id
51
+ end
52
+
53
+ # use populator for default value on scalars?
54
+
55
+ # adding to collection via :populator.
56
+ # valid.
57
+ it "yyy" do
58
+ form.validate(
59
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
60
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
61
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
62
+ ).must_equal true
63
+
64
+ form.errors.messages.inspect.must_equal "{}"
65
+
66
+ # form has updated.
67
+ form.name.must_equal "The Dissent Of Man"
68
+ form.songs[0].title.must_equal "Fallout"
69
+ form.songs[1].title.must_equal "Roxanne"
70
+ form.songs[1].composer.name.must_equal "Greg Graffin"
71
+
72
+ form.songs[1].composer.model.must_be_instance_of Artist
73
+
74
+ form.songs[1].title.must_equal "Roxanne"
75
+ form.songs[2].title.must_equal "Rime Of The Ancient Mariner" # new song added.
76
+ form.songs[3].title.must_equal "Re-Education"
77
+ form.songs[3].composer.name.must_equal "Rise Against"
78
+ form.songs.size.must_equal 4
79
+ form.artist.name.must_equal "Bad Religion"
80
+
81
+
82
+ # model has not changed, yet.
83
+ album.name.must_equal "The Dissent Of Man"
84
+ album.songs[0].title.must_equal "Broken"
85
+ album.songs[1].title.must_equal "Resist Stance"
86
+ album.songs[1].composer.name.must_equal "Greg Graffin"
87
+ album.songs.size.must_equal 2
88
+ album.artist.name.must_equal "Bad Religion"
89
+ end
90
+ end
91
+
92
+ class PopulateIfEmptyTest < MiniTest::Spec
93
+ Song = Struct.new(:title, :album, :composer)
94
+ Album = Struct.new(:name, :songs, :artist)
95
+ Artist = Struct.new(:name)
96
+
97
+ let (:song) { Song.new("Broken") }
98
+ let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
99
+ let (:composer) { Artist.new("Greg Graffin") }
100
+ let (:artist) { Artist.new("Bad Religion") }
101
+ let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
102
+
103
+
104
+
105
+ class AlbumForm < Reform::Form
106
+ property :name
107
+
108
+ collection :songs,
109
+ populate_if_empty: Song do # class name works.
110
+
111
+ property :title
112
+ validates :title, presence: true
113
+
114
+ property :composer, populate_if_empty: :populate_composer! do # lambda works, too. in form context.
115
+ property :name
116
+ validates :name, presence: true
117
+ end
118
+
119
+ private
120
+ def populate_composer!(fragment, options)
121
+ Artist.new
122
+ end
123
+ end
124
+
125
+ property :artist, populate_if_empty: lambda { |*args| create_artist(args) } do # methods work, too.
126
+ property :name
127
+ end
128
+
129
+ private
130
+ class Sting < Artist
131
+ attr_accessor :args
132
+ end
133
+ def create_artist(args)
134
+ Sting.new.tap { |artist| artist.args=(args) }
135
+ end
136
+ end
137
+
138
+ let (:form) { AlbumForm.new(album) }
139
+
140
+ it do
141
+ form.validate(
142
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
143
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
144
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
145
+ ).must_equal true
146
+
147
+ form.errors.messages.inspect.must_equal "{}"
148
+
149
+ # form has updated.
150
+ form.name.must_equal "The Dissent Of Man"
151
+ form.songs[0].title.must_equal "Fallout"
152
+ form.songs[1].title.must_equal "Roxanne"
153
+ form.songs[1].composer.name.must_equal "Greg Graffin"
154
+ form.songs[1].title.must_equal "Roxanne"
155
+ form.songs[2].title.must_equal "Rime Of The Ancient Mariner" # new song added.
156
+ form.songs[3].title.must_equal "Re-Education"
157
+ form.songs[3].composer.name.must_equal "Rise Against"
158
+ form.songs.size.must_equal 4
159
+ form.artist.name.must_equal "Bad Religion"
160
+
161
+
162
+ # model has not changed, yet.
163
+ album.name.must_equal "The Dissent Of Man"
164
+ album.songs[0].title.must_equal "Broken"
165
+ album.songs[1].title.must_equal "Resist Stance"
166
+ album.songs[1].composer.name.must_equal "Greg Graffin"
167
+ album.songs.size.must_equal 2
168
+ album.artist.name.must_equal "Bad Religion"
169
+ end
170
+
171
+ # trigger artist populator. lambda calling form instance method.
172
+ it do
173
+ form = AlbumForm.new(album = Album.new)
174
+ form.validate("artist" => {"name" => "From Autumn To Ashes"})
175
+
176
+ form.artist.name.must_equal "From Autumn To Ashes"
177
+ # test lambda was executed in form context.
178
+ form.artist.model.must_be_instance_of AlbumForm::Sting
179
+ # test lambda block arguments.
180
+ form.artist.model.args.to_s.must_equal "[{\"name\"=>\"From Autumn To Ashes\"}, {}]"
181
+
182
+ album.artist.must_equal nil
183
+ end
184
+
185
+ end
186
+
187
+
188
+ # delete songs while deserializing.
189
+ class PopulateIfEmptyWithDeletionTest < MiniTest::Spec
190
+ Song = Struct.new(:title, :album, :composer)
191
+ Album = Struct.new(:name, :songs, :artist)
192
+
193
+ let (:song) { Song.new("Broken") }
194
+ let (:song2) { Song.new("Resist Stance") }
195
+ let (:album) { Album.new("The Dissent Of Man", [song, song2]) }
196
+
197
+
198
+ class AlbumForm < Reform::Form
199
+ property :name
200
+
201
+ collection :songs,
202
+ populate_if_empty: Song, skip_if: :delete_song! do
203
+
204
+ property :title
205
+ validates :title, presence: true
206
+ end
207
+
208
+ def delete_song!(fragment, *)
209
+ songs.delete(songs[0]) and return true if fragment["title"] == "Broken, delete me!"
210
+ false
211
+ end
212
+ end
213
+
214
+ let (:form) { AlbumForm.new(album) }
215
+
216
+ it do
217
+ form.validate(
218
+ "songs" => [{"title" => "Broken, delete me!"}, {"title" => "Roxanne"}]
219
+ ).must_equal true
220
+
221
+ form.errors.messages.inspect.must_equal "{}"
222
+
223
+ form.songs.size.must_equal 1
224
+ form.songs[0].title.must_equal "Roxanne"
225
+ end
226
+ end
@@ -0,0 +1,112 @@
1
+ require 'test_helper'
2
+
3
+ class PrepopulatorTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :band, :length)
5
+ Band = Struct.new(:name)
6
+
7
+ class AlbumForm < Reform::Form
8
+ property :title, prepopulator: ->(*){ self.title = "Another Day At Work" } # normal assignment.
9
+ property :length
10
+
11
+ property :hit, prepopulator: ->(options) { self.hit = Song.new(options[:title]) } do # use user options.
12
+ property :title
13
+
14
+ property :band, prepopulator: ->(options){ self.band = my_band(options[:title]) } do # invoke your own code.
15
+ property :name
16
+ end
17
+
18
+ def my_band(name)
19
+ Band.new(title)
20
+ end
21
+ end
22
+
23
+ collection :songs, prepopulator: :prepopulate_songs! do
24
+ property :title
25
+ end
26
+
27
+ private
28
+ def prepopulate_songs!(options)
29
+ if songs == nil
30
+ self.songs = [Song.new, Song.new]
31
+ else
32
+ songs << Song.new # full Twin::Collection API available.
33
+ end
34
+ end
35
+ end
36
+
37
+ it do
38
+ form = AlbumForm.new(OpenStruct.new(length: 1)).prepopulate!(title: "Potemkin City Limits")
39
+
40
+ form.length.must_equal 1
41
+ form.title.must_equal "Another Day At Work"
42
+ form.hit.model.must_equal Song.new("Potemkin City Limits")
43
+ form.songs.size.must_equal 2
44
+ form.songs[0].model.must_equal Song.new
45
+ form.songs[1].model.must_equal Song.new
46
+ form.songs[1].model.must_equal Song.new
47
+ # prepopulate works more than 1 level, recursive.
48
+ # it also passes options properly down there.
49
+ form.hit.band.model.must_equal Band.new("Potemkin City Limits")
50
+ end
51
+
52
+ # add to existing collection.
53
+ it do
54
+ form = AlbumForm.new(OpenStruct.new(songs: [Song.new])).prepopulate!
55
+
56
+ form.songs.size.must_equal 2
57
+ form.songs[0].model.must_equal Song.new
58
+ form.songs[1].model.must_equal Song.new
59
+ end
60
+ end
61
+
62
+ # calling form.prepopulate! shouldn't crash.
63
+ class PrepopulateWithoutConfiguration < MiniTest::Spec
64
+ Song = Struct.new(:title)
65
+
66
+ class AlbumForm < Reform::Form
67
+ collection :songs do
68
+ property :title
69
+ end
70
+
71
+ property :hit do
72
+ property :title
73
+ end
74
+ end
75
+
76
+ subject { AlbumForm.new(OpenStruct.new(songs: [], hit: nil)).prepopulate! }
77
+
78
+ it { subject.songs.size.must_equal 0 }
79
+ end
80
+
81
+
82
+ class ManualPrepopulatorOverridingTest < MiniTest::Spec
83
+ Song = Struct.new(:title, :band, :length)
84
+ Band = Struct.new(:name)
85
+
86
+ class AlbumForm < Reform::Form
87
+ property :title
88
+ property :length
89
+
90
+ property :hit do
91
+ property :title
92
+
93
+ property :band do
94
+ property :name
95
+ end
96
+ end
97
+
98
+ def prepopulate!(options)
99
+ self.hit = Song.new(options[:title])
100
+ super
101
+ end
102
+ end
103
+
104
+ # you can simply override Form#prepopulate!
105
+ it do
106
+ form = AlbumForm.new(OpenStruct.new(length: 1)).prepopulate!(title: "Potemkin City Limits")
107
+
108
+ form.length.must_equal 1
109
+ form.hit.model.must_equal Song.new("Potemkin City Limits")
110
+ form.hit.title.must_equal "Potemkin City Limits"
111
+ end
112
+ end
@@ -4,8 +4,6 @@ class ReadableTest < MiniTest::Spec
4
4
  Credentials = Struct.new(:password)
5
5
 
6
6
  class PasswordForm < Reform::Form
7
- reform_2_0!
8
-
9
7
  property :password, readable: false
10
8
  end
11
9
 
@@ -13,14 +11,14 @@ class ReadableTest < MiniTest::Spec
13
11
  let (:form) { PasswordForm.new(cred) }
14
12
 
15
13
  it {
16
- form.password.must_equal nil
14
+ form.password.must_equal nil # password not read.
17
15
 
18
16
  form.validate("password" => "123")
19
17
 
20
18
  form.password.must_equal "123"
21
19
 
22
20
  form.sync
23
- cred.password.must_equal "123"
21
+ cred.password.must_equal "123" # password written.
24
22
 
25
23
  hash = {}
26
24
  form.save do |nested|
data/test/save_test.rb CHANGED
@@ -1,139 +1,83 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class SaveTest < BaseTest
4
- class AlbumForm < Reform::Form
5
- property :title
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :songs, :artist)
6
+ Artist = Struct.new(:name)
6
7
 
7
- property :hit do
8
- property :title
9
- validates :title, :presence => true
10
- end
8
+ class AlbumForm < Reform::Form
9
+ property :name
10
+ validates :name, presence: true
11
11
 
12
12
  collection :songs do
13
13
  property :title
14
- validates :title, :presence => true
15
- end
14
+ validates :title, presence: true
16
15
 
17
- property :band do # yepp, people do crazy stuff like that.
18
- property :label do
16
+ property :composer do
19
17
  property :name
20
- validates :name, :presence => true
18
+ validates :name, presence: true
21
19
  end
22
- # TODO: make band a required object.
23
20
  end
24
21
 
25
- validates :title, :presence => true
22
+ property :artist, save: false do
23
+ property :name
24
+ end
26
25
  end
27
26
 
28
- let (:params) {
29
- {
30
- "title" => "Best Of",
31
- "hit" => {"title" => "Roxanne"},
32
- "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"}],
33
- :band => {:label => {:name => "Polydor"}}
34
- }
35
- }
36
-
37
- let (:album) { Album.new(nil, hit, [song1, song2], band) }
38
- let (:hit) { Song.new }
39
- let (:song1) { Song.new }
40
- let (:song2) { Song.new }
41
- let (:band) { Band.new(label) }
42
- let (:label) { Label.new }
43
-
44
- subject { AlbumForm.new(album) }
45
-
46
- before do
47
- [album, hit, song1, song2, band, label].each { |mdl| mdl.extend(Saveable) }
48
-
49
- subject.validate(params)
50
- subject.save
51
- end
27
+ module Saveable
28
+ def save
29
+ @saved = true
30
+ end
52
31
 
53
- # synced?
54
- it { album.title.must_equal "Best Of" }
55
- it { hit.title.must_equal "Roxanne" }
56
- it { song1.title.must_equal "Fallout" }
57
- it { song2.title.must_equal "Roxanne" }
58
- it { label.name.must_equal "Polydor" }
59
-
60
- # saved?
61
- it { album.saved?.must_equal true }
62
- it { hit.saved?.must_equal true }
63
- it { song1.saved?.must_equal true }
64
- it { song1.saved?.must_equal true }
65
- it { band.saved?.must_equal true }
66
- it { label.saved?.must_equal true }
67
-
68
-
69
- describe "save: false" do
70
- let (:form) {
71
- Class.new(Reform::Form) do
72
- property :hit do
73
- property :title
74
- end
75
-
76
- collection :songs, :save => false do
77
- property :title
78
- end
79
-
80
- property :band do # yepp, people do crazy stuff like that.
81
- property :label, :save => false do
82
- property :name
83
- end
84
- # TODO: make band a required object.
85
- end
86
- end
87
- }
88
-
89
- subject { form.new(album) }
90
-
91
- # synced?
92
- it { hit.title.must_equal "Roxanne" }
93
- it { song1.title.must_equal "Fallout" }
94
- it { song2.title.must_equal "Roxanne" }
95
- it { label.name.must_equal "Polydor" }
96
-
97
- # saved?
98
- it { album.saved?.must_equal true }
99
- it { hit.saved?.must_equal true }
100
- it { song1.saved?.must_equal nil }
101
- it { song1.saved?.must_equal nil }
102
- it { band.saved?.must_equal true }
103
- it { label.saved?.must_equal nil }
32
+ def saved?
33
+ @saved
34
+ end
104
35
  end
105
36
 
106
37
 
107
- # #save returns result (this goes into disposable soon).
108
- it { subject.save.must_equal true }
38
+ let (:song) { Song.new("Broken").extend(Saveable) }
39
+ # let (:song_with_composer) { Song.new("Resist Stance", nil, composer).extend(Saveable) }
40
+ let (:composer) { Artist.new("Greg Graffin").extend(Saveable) }
41
+ let (:artist) { Artist.new("Bad Religion").extend(Saveable).extend(Saveable) }
42
+ let (:album) { Album.new("The Dissent Of Man", [song], artist).extend(Saveable) }
43
+
44
+ let (:form) { AlbumForm.new(album) }
45
+
46
+
109
47
  it do
110
- album.instance_eval { def save; false; end }
111
- subject.save.must_equal false
48
+ form.validate("songs" => [{"title" => "Fixed"}])
49
+
50
+ form.save
51
+
52
+ album.saved?.must_equal true
53
+ album.songs[0].title.must_equal "Fixed"
54
+ album.songs[0].saved?.must_equal true
55
+ album.artist.saved?.must_equal nil
112
56
  end
113
57
  end
114
58
 
115
59
 
116
- class SaveWithDynamicOptionsTest < MiniTest::Spec
117
- Song = Struct.new(:id, :title, :length) do
118
- include Saveable
119
- end
60
+ # class SaveWithDynamicOptionsTest < MiniTest::Spec
61
+ # Song = Struct.new(:id, :title, :length) do
62
+ # include Saveable
63
+ # end
120
64
 
121
- class SongForm < Reform::Form
122
- property :title#, save: false
123
- property :length, virtual: true
124
- end
65
+ # class SongForm < Reform::Form
66
+ # property :title#, save: false
67
+ # property :length, virtual: true
68
+ # end
125
69
 
126
- let (:song) { Song.new }
127
- let (:form) { SongForm.new(song) }
70
+ # let (:song) { Song.new }
71
+ # let (:form) { SongForm.new(song) }
128
72
 
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}" })
73
+ # # we have access to original input value and outside parameters.
74
+ # it "xxx" do
75
+ # form.validate("title" => "A Poor Man's Memory", "length" => 10)
76
+ # length_seconds = 120
77
+ # form.save(length: lambda { |value, options| form.model.id = "#{value}: #{length_seconds}" })
134
78
 
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
139
- end
79
+ # song.title.must_equal "A Poor Man's Memory"
80
+ # song.length.must_equal nil
81
+ # song.id.must_equal "10: 120"
82
+ # end
83
+ # end