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
@@ -1,84 +0,0 @@
1
- require 'representable/hash'
2
- require 'representable/decorator'
3
-
4
- module Reform
5
- class Representer < Representable::Decorator
6
- include Representable::Hash::AllowSymbols
7
-
8
- extend Uber::InheritableAttr
9
- inheritable_attr :options # FIXME: this doesn't need to be inheritable.
10
- # self.options = {}
11
-
12
-
13
- class Options < ::Hash
14
- def include!(names)
15
- includes.push(*names) #if names.size > 0
16
- self
17
- end
18
-
19
- def exclude!(names)
20
- excludes.push(*names) #if names.size > 0
21
- self
22
- end
23
-
24
- def excludes
25
- self[:exclude] ||= []
26
- end
27
-
28
- def includes
29
- self[:include] ||= []
30
- end
31
- end
32
-
33
-
34
- include Representable::Hash
35
-
36
- # Returns hash of all property names.
37
- def self.fields(&block)
38
- representable_attrs.find_all(&block).map(&:name)
39
- end
40
-
41
- def self.each(only_form=true, &block)
42
- definitions = representable_attrs
43
- definitions = representable_attrs.find_all { |attr| attr[:form] } if only_form
44
-
45
- definitions.each(&block)
46
- self
47
- end
48
-
49
- def self.for(options)
50
- clone.tap do |representer|
51
- representer.options = options
52
- end
53
- end
54
-
55
- def self.default_inline_class
56
- options[:form_class]
57
- end
58
-
59
- def self.clone # called in inheritable_attr :representer_class.
60
- Class.new(self) # By subclassing, representable_attrs.clone is called.
61
- end
62
-
63
- private
64
-
65
- # Inline forms always get saved in :extend.
66
- def self.build_inline(base, features, name, options, &block)
67
- name = name.to_s.singularize.camelize
68
-
69
- features = options[:features]
70
-
71
- Class.new(base || default_inline_class) do
72
- include *features
73
-
74
- class_eval &block
75
-
76
- @form_name = name
77
-
78
- def self.name # needed by ActiveModel::Validation and I18N.
79
- @form_name
80
- end
81
- end
82
- end
83
- end
84
- end
data/test/empty_test.rb DELETED
@@ -1,58 +0,0 @@
1
- require 'test_helper'
2
-
3
-
4
- class DeprecatedVirtualTest < MiniTest::Spec # TODO: remove me in 2.0.
5
- Location = Struct.new(:country)
6
-
7
- class LocationForm < Reform::Form
8
- property :country, virtual: true # this becomes readonly: true
9
- end
10
-
11
- let (:loc) { Location.new("Australia") }
12
- let (:form) { LocationForm.new(loc) }
13
-
14
- it { form.country.must_equal "Australia" }
15
- it do
16
- form.validate("country" => "Germany") # this usually won't change when submitting.
17
- form.country.must_equal "Germany"
18
-
19
- form.sync
20
- loc.country.must_equal "Australia" # the writer wasn't called.
21
-
22
- hash = {}
23
- form.save do |nested|
24
- hash = nested
25
- end
26
-
27
- hash.must_equal("country"=> "Germany")
28
- end
29
- end
30
-
31
- class DeprecatedEmptyTest < MiniTest::Spec # don't read, don't write
32
- Credentials = Struct.new(:password)
33
-
34
- class PasswordForm < Reform::Form
35
- property :password
36
- property :password_confirmation, empty: true
37
- end
38
-
39
- let (:cred) { Credentials.new }
40
- let (:form) { PasswordForm.new(cred) }
41
-
42
- it {
43
- form.validate("password" => "123", "password_confirmation" => "321")
44
-
45
- form.password.must_equal "123"
46
- form.password_confirmation.must_equal "321" # this is still readable in the UI.
47
-
48
- form.sync
49
- cred.password.must_equal "123"
50
-
51
- hash = {}
52
- form.save do |nested|
53
- hash = nested
54
- end
55
-
56
- hash.must_equal("password"=> "123", "password_confirmation" => "321")
57
- }
58
- end
@@ -1,145 +0,0 @@
1
- require 'test_helper'
2
-
3
- class FormCompositionTest < MiniTest::Spec
4
- Song = Struct.new(:id, :title, :band)
5
- Requester = Struct.new(:id, :name, :requester)
6
- Band = Struct.new(:title)
7
-
8
- class RequestForm < Reform::Form
9
- include Composition
10
-
11
- property :name, :on => :requester
12
- property :requester_id, :on => :requester, :from => :id
13
- properties :title, :id, :on => :song
14
- # property :channel # FIXME: what about the "main model"?
15
- property :channel, :empty => true, :on => :song
16
- property :requester, :on => :requester
17
- property :captcha, :on => :song, :empty => true
18
-
19
- validates :name, :title, :channel, :presence => true
20
-
21
- property :band, :on => :song do
22
- property :title
23
- end
24
- end
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") }
30
-
31
- # delegation form -> composition works
32
- it { form.id.must_equal 1 }
33
- it { form.title.must_equal "Rio" }
34
- it { form.name.must_equal "Duran Duran" }
35
- it { form.requester_id.must_equal 2 }
36
- it { form.channel.must_equal nil }
37
- it { form.requester.must_equal "MCP" } # same name as composed model.
38
- it { form.captcha.must_equal nil }
39
-
40
- # #model just returns <Composition>.
41
- it { form.model.must_be_kind_of Reform::Composition }
42
-
43
- # #model[] -> composed models
44
- it { form.model[:requester].must_equal requester }
45
- it { form.model[:song].must_equal song }
46
-
47
-
48
- it "creates Composition for you" do
49
- form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
50
- end
51
-
52
- describe "#save" do
53
- # #save with {}
54
- it do
55
- hash = {}
56
-
57
- form.save do |map|
58
- hash[:name] = form.name
59
- hash[:title] = form.title
60
- end
61
-
62
- hash.must_equal({:name=>"Duran Duran", :title=>"Rio"})
63
- end
64
-
65
- it "provides nested symbolized hash as second block argument" do
66
- form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "channel" => "JJJ", "captcha" => "wonderful")
67
-
68
- hash = nil
69
-
70
- form.save do |map|
71
- hash = map
72
- end
73
-
74
- hash.must_equal({
75
- :song=>{:title=>"Greyhound", :id=>1, :channel => "JJJ", :captcha=>"wonderful", :band=>{"title"=>"Duran^2"}},
76
- :requester=>{:name=>"Frenzal Rhomb", :id=>2, :requester => "MCP"}
77
- }
78
- )
79
- end
80
-
81
- it "xxx pushes data to models and calls #save when no block passed" do
82
- song.extend(Saveable)
83
- requester.extend(Saveable)
84
- band.extend(Saveable)
85
-
86
- form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
87
- form.captcha.must_equal "1337" # TODO: move to separate test.
88
-
89
- form.save
90
-
91
- requester.name.must_equal "Frenzal Rhomb"
92
- requester.saved?.must_equal true
93
- song.title.must_equal "Greyhound"
94
- song.saved?.must_equal true
95
- song.band.title.must_equal "Duran^2"
96
- song.band.saved?.must_equal true
97
- end
98
-
99
- it "returns true when models all save successfully" do
100
- song.extend(Saveable)
101
- requester.extend(Saveable)
102
- band.extend(Saveable)
103
-
104
- form.save.must_equal true
105
- end
106
-
107
- it "returns false when one or more models don't save successfully" do
108
- module Unsaveable
109
- def save
110
- false
111
- end
112
- end
113
-
114
- song.extend(Unsaveable)
115
- requester.extend(Saveable)
116
- band.extend(Saveable)
117
-
118
- form.save.must_equal false
119
- end
120
- end
121
- end
122
-
123
-
124
- class FormCompositionCollectionTest < MiniTest::Spec
125
- Book = Struct.new(:id, :name)
126
- Library = Struct.new(:id) do
127
- def books
128
- [Book.new(1,"My book")]
129
- end
130
- end
131
-
132
- class LibraryForm < Reform::Form
133
- include Reform::Form::Composition
134
-
135
- collection :books, on: :library do
136
- property :id
137
- property :name
138
- end
139
- end
140
-
141
- let (:form) { LibraryForm.new(library: library) }
142
- let (:library) { Library.new(2) }
143
-
144
- it { form.save do |hash| hash.must_equal({:library=>{:books=>[{"id"=>1, "name"=>"My book"}]}}) end }
145
- end
@@ -1,197 +0,0 @@
1
- require 'test_helper'
2
-
3
- class NestedFormTest < MiniTest::Spec
4
- class AlbumForm < Reform::Form
5
- property :title
6
-
7
- # class SongForm < Reform::Form
8
- # property :title
9
- # validates :title, :presence => true
10
- # end
11
-
12
- #form :hit, :class => SongForm
13
- property :hit do
14
- property :title
15
- validates :title, :presence => true
16
- end
17
-
18
- collection :songs do
19
- property :title
20
- validates :title, :presence => true
21
- end
22
-
23
- validates :title, :presence => true
24
- end
25
-
26
- # AlbumForm::collection :songs, :form => SongForm
27
- # should be: AlbumForm.new(songs: [Song, Song])
28
-
29
- let (:album) do
30
- OpenStruct.new(
31
- :title => "Blackhawks Over Los Angeles",
32
- :hit => song,
33
- :songs => songs # TODO: document this requirement
34
- )
35
- end
36
- let (:song) { OpenStruct.new(:title => "Downtown") }
37
- let (:songs) { [OpenStruct.new(:title => "Calling")] }
38
- let (:form) { AlbumForm.new(album) }
39
-
40
- it "responds to #save" do
41
- hsh = nil
42
- form.save do |nested|
43
- hsh = nested
44
- end
45
- hsh.must_equal({"hit"=>{"title"=>"Downtown"}, "title" => "Blackhawks Over Los Angeles", "songs"=>[{"title"=>"Calling"}]})
46
- end
47
-
48
-
49
- it "creates nested forms" do
50
- form.hit.must_be_kind_of Reform::Form
51
- form.songs.must_be_kind_of Array
52
- end
53
-
54
- describe "#initialize" do
55
- describe "with empty object and no cardinality" do
56
- let(:form) { AlbumForm.new(OpenStruct.new) }
57
-
58
- it "allows initialization with empty properties" do
59
- form
60
- end
61
-
62
- it "allows #validate" do
63
- form.validate({})
64
- form.errors.messages.must_equal(:title=>["can't be blank"])
65
- end
66
- # it "must support #validate when initialized with empty properties" do
67
- # form.validate({})
68
- # form.errors.messages.must_equal(:title=>["can't be blank"], :"hit.title"=>["can't be blank"], :"songs.title"=>["can't be blank"])
69
- # end
70
- # it "must support #validate with attributes when initialized with empty properties" do
71
- # form.validate("hit"=>{"title"=>"Downtown"}, "title" => "Blackhawks Over Los Angeles", "songs"=>[{"title"=>"Calling"}])
72
- # form.title.must_eql "Blackhawks Over Los Angeles"
73
- # form.errors.messages.must_equal([])
74
- # end
75
- end
76
-
77
-
78
- end
79
-
80
-
81
- describe "rendering" do
82
- it { form.title.must_equal "Blackhawks Over Los Angeles" }
83
- it { form.hit.title.must_equal "Downtown" }
84
- it { form.songs[0].title.must_equal "Calling" }
85
- end
86
-
87
- describe "#save" do
88
- before { @result = form.validate(
89
- "hit" =>{"title" => "Sacrifice"},
90
- "title" =>"Second Heat",
91
- "songs" => [{"title" => "Scarified"}])
92
- }
93
-
94
- it "updates internal Fields" do
95
- data = {}
96
-
97
- form.save do |nested_hash|
98
- data[:title] = form.title
99
- data[:hit_title] = form.hit.title
100
- data[:first_title] = form.songs.first.title
101
- end
102
-
103
- data.must_equal(:title=>"Second Heat", :hit_title => "Sacrifice", :first_title => "Scarified")
104
- end
105
-
106
- it "returns nested hash with indifferent access" do
107
- nested = nil
108
-
109
- form.save do |nested_hash|
110
- nested = nested_hash
111
- end
112
-
113
- nested.must_equal("title"=>"Second Heat", "hit"=>{"title"=>"Sacrifice"}, "songs"=>[{"title"=>"Scarified"}])
114
-
115
- nested[:title].must_equal "Second Heat"
116
- nested["title"].must_equal "Second Heat"
117
- nested[:hit][:title].must_equal "Sacrifice"
118
- nested["hit"]["title"].must_equal "Sacrifice"
119
- end
120
-
121
- it "pushes data to models" do
122
- form.save
123
-
124
- album.title.must_equal "Second Heat"
125
- song.title.must_equal "Sacrifice"
126
- songs.first.title.must_equal "Scarified"
127
- end
128
-
129
- describe "with invalid args" do
130
- it "allows empty collection values" do
131
- form.validate({})
132
-
133
- form.songs.size.must_equal 1
134
- form.songs[0].title.must_equal "Scarified"
135
- end
136
- end
137
- end
138
-
139
- # describe "with aliased nested form name" do
140
- # let (:form) do
141
- # Class.new(Reform::Form) do
142
- # form :hit, :class => AlbumForm::SongForm, :as => :song
143
- # end.new(OpenStruct.new(:hit => OpenStruct.new(:title => "")))
144
- # end
145
-
146
- # it "uses alias in errors" do
147
- # form.validate({})
148
- # form.errors.messages.must_equal({})
149
- # end
150
- # end
151
-
152
- class ExplicitNestedFormTest < MiniTest::Spec
153
- let (:song) { OpenStruct.new(:title => "Downtown") }
154
- let (:album) do
155
- OpenStruct.new(
156
- :title => "Blackhawks Over Los Angeles",
157
- :hit => song,
158
- )
159
- end
160
- let (:form) { AlbumForm.new(album) }
161
-
162
- class SongForm < Reform::Form
163
- property :title
164
- validates_presence_of :title
165
- end
166
-
167
- class AlbumForm < Reform::Form
168
- property :title
169
-
170
- property :hit, :form => SongForm #, :parse_strategy => :sync, :instance => true
171
- end
172
-
173
-
174
- it "allows rendering" do
175
- form.hit.title.must_equal "Downtown"
176
- end
177
-
178
- it ("xxx") {
179
- form.validate({"hit" => {"title" => ""}})
180
- form.hit.title.must_equal ""
181
- form.errors[:"hit.title"].must_equal(["can't be blank"])
182
- }
183
- end
184
-
185
- class UnitTest < self
186
- it "keeps Forms for form collection" do
187
- form.send(:fields).songs.must_be_kind_of Array
188
- end
189
-
190
- describe "#validate" do
191
- it "keeps Form instances" do
192
- form.validate("songs"=>[{"title" => "Atwa"}])
193
- form.songs.first.must_be_kind_of Reform::Form
194
- end
195
- end
196
- end
197
- end