reform 1.2.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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