reform 1.2.0.beta2 → 1.2.1

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.
@@ -10,4 +10,4 @@ module Reform
10
10
  :from_json
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -1,23 +1,8 @@
1
1
  module Reform::Form::Save
2
- module RecursiveSave
3
- def to_hash(*)
4
- # process output from InputRepresenter {title: "Mint Car", hit: <Form>}
5
- # and just call sync! on nested forms.
6
- nested_forms do |attr|
7
- attr.merge!(
8
- :instance => lambda { |fragment, *| fragment },
9
- :serialize => lambda { |object, args| object.save! unless args.binding[:save] === false },
10
- )
11
- end
12
-
13
- super
14
- end
15
- end
16
-
17
2
  # Returns the result of that save invocation on the model.
18
3
  def save(options={}, &block)
19
4
  # DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
20
- return deprecate_first_save_block_arg(&block) if block_given?
5
+ return yield to_nested_hash if block_given?
21
6
 
22
7
  sync_models # recursion
23
8
  save!(options)
@@ -25,15 +10,10 @@ module Reform::Form::Save
25
10
 
26
11
  def save!(options={}) # FIXME.
27
12
  result = save_model
28
- mapper.new(fields).extend(RecursiveSave).to_hash # save! on all nested forms. # TODO: only include nested forms here.
29
13
 
30
- names = options.keys & changed.keys.map(&:to_sym)
31
- if names.size > 0
32
- representer = save_dynamic_representer.new(fields) # should be done once, on class instance level.
14
+ save_representer.new(fields).to_hash # save! on all nested forms.
33
15
 
34
- # puts "$$$$$$$$$ #{names.inspect}"
35
- representer.to_hash(options.merge :include => names)
36
- end
16
+ dynamic_save!(options)
37
17
 
38
18
  result
39
19
  end
@@ -43,54 +23,44 @@ module Reform::Form::Save
43
23
  end
44
24
 
45
25
 
46
- def save_dynamic_representer
47
- # puts mapper.superclass.superclass.inspect
48
- Class.new(mapper).apply do |dfn|
26
+ require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
27
+ def to_nested_hash(*)
28
+ ActiveSupport::HashWithIndifferentAccess.new(nested_hash_representer.new(fields).to_hash)
29
+ end
30
+ alias_method :to_hash, :to_nested_hash
31
+ # NOTE: it is not recommended using #to_hash and #to_nested_hash in your code, consider them private.
32
+
33
+ private
34
+ def save_representer
35
+ self.class.representer(:save) do |dfn|
49
36
  dfn.merge!(
50
- :serialize => lambda { |object, options|
51
- puts "$$ #{options.user_options.inspect}"
52
- options.user_options[options.binding.name.to_sym].call(object, options) },
53
- :representable => true
37
+ :instance => lambda { |form, *| form },
38
+ :serialize => lambda { |form, args| form.save! unless args.binding[:save] === false }
54
39
  )
55
40
  end
56
41
  end
57
42
 
43
+ def nested_hash_representer
44
+ self.class.representer(:nested_hash, :all => true) do |dfn|
45
+ dfn.merge!(:serialize => lambda { |form, args| form.to_nested_hash }) if dfn[:form]
58
46
 
59
- module NestedHash
60
- def to_hash(*)
61
- # Transform form data into a nested hash for #save.
62
- nested_forms do |attr|
63
- attr.merge!(
64
- :serialize => lambda { |object, args| object.to_nested_hash }
65
- )
66
- end
67
-
68
- representable_attrs.each do |attr|
69
- attr.merge!(:as => attr[:private_name] || attr.name)
70
- end
71
-
72
- super
47
+ dfn.merge!(:as => dfn[:private_name] || dfn.name)
73
48
  end
74
49
  end
75
50
 
51
+ def dynamic_save!(options)
52
+ names = options.keys & changed.keys.map(&:to_sym)
53
+ return if names.size == 0
76
54
 
77
- require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
78
- def to_nested_hash(*)
79
- map = mapper.new(fields).extend(NestedHash)
80
-
81
- ActiveSupport::HashWithIndifferentAccess.new(map.to_hash)
55
+ dynamic_save_representer.new(fields).to_hash(options.merge(:include => names))
82
56
  end
83
- alias_method :to_hash, :to_nested_hash
84
- # NOTE: it is not recommended using #to_hash and #to_nested_hash in your code, consider
85
- # them private.
86
57
 
87
- private
88
- def deprecate_first_save_block_arg(&block)
89
- if block.arity == 2
90
- warn "[Reform] Deprecation Warning: The first block argument in `save { |form, hash| .. }` is deprecated and its new signature is `save { |hash| .. }`. If you need the form instance, use it in the block. Have a good day."
91
- return yield(self, to_nested_hash)
58
+ def dynamic_save_representer
59
+ self.class.representer(:dynamic_save, :all => true) do |dfn|
60
+ dfn.merge!(
61
+ :serialize => lambda { |object, options| options.user_options[options.binding.name.to_sym].call(object, options) },
62
+ :representable => true
63
+ )
92
64
  end
93
-
94
- yield to_nested_hash # new behaviour.
95
65
  end
96
66
  end
@@ -2,113 +2,95 @@
2
2
  # 1. assign scalars to model (respecting virtual, excluded attributes)
3
3
  # 2. call sync! on nested
4
4
  module Reform::Form::Sync
5
- # Mechanics for writing input to model.
6
- # Writes input to model.
7
- module Writer
8
- def from_hash(*)
9
- # process output from InputRepresenter {title: "Mint Car", hit: <Form>}
10
- # and just call sync! on nested forms.
11
- nested_forms do |attr|
12
- attr.merge!(
13
- :instance => lambda { |fragment, *| fragment },
14
- # FIXME: do we allow options for #sync for nested forms?
15
- :deserialize => lambda { |object, *| model = object.sync!({}) } # sync! returns the synced model.
16
- # representable's :setter will do collection=([..]) or property=(..) for us on the model.
17
- )
18
- end
19
-
20
- super
21
- end
5
+ def sync_models(options={})
6
+ sync!(options)
22
7
  end
8
+ alias_method :sync, :sync_models
23
9
 
24
- module Setter
25
- def from_hash(*)
26
- clone_config!
10
+ # reading from fields allows using readers in form for presentation
11
+ # and writers still pass to fields in #validate????
12
+ def sync!(options) # semi-public.
13
+ options = Reform::Representer::Options[options.merge(:form => self)] # options local for this form, only.
27
14
 
28
- representable_attrs.each do |dfn|
29
- next unless setter = dfn[:sync]
15
+ input = sync_hash(options)
16
+ # if aliased_model was a proper Twin, we could do changed? stuff there.
30
17
 
31
- setter_proc = lambda do |value, options|
32
- # puts "~~ #{value}~ #{options.user_options.inspect}"
18
+ options.delete(:exclude) # TODO: can we use 2 options?
33
19
 
34
- if options.binding[:sync] == true
35
- options.user_options[options.binding.name.to_sym].call(value, options)
36
- next
37
- end
20
+ dynamic_sync_representer.new(aliased_model).from_hash(input, options) # sync properties to Song.
38
21
 
39
- # evaluate the :sync block in form context (should we do that everywhere?).
40
- options.user_options[:form].instance_exec(value, options, &setter)
41
- end
22
+ model
23
+ end
42
24
 
43
- dfn.merge!(:setter => setter_proc)
44
- end
25
+ private
45
26
 
46
- super
27
+ # Transforms form input into what actually gets written to model.
28
+ # output: {title: "Mint Car", hit: <Form>}
29
+ def input_representer
30
+ self.class.representer(:input) do |dfn|
31
+ dfn.merge!(
32
+ :representable => false,
33
+ :prepare => lambda { |obj, *| obj }
34
+ )
47
35
  end
48
36
  end
49
37
 
50
- # Transforms form input into what actually gets written to model.
51
- # output: {title: "Mint Car", hit: <Form>}
52
- module InputRepresenter
53
- # receives Representer::Options hash.
54
- def to_hash(*)
55
- nested_forms do |attr|
56
- attr.merge!(
57
- :representable => false,
58
- :prepare => lambda { |obj, *| obj }
38
+ # Writes input to model.
39
+ def sync_representer
40
+ self.class.representer(:sync, :all => true) do |dfn|
41
+ if dfn[:form]
42
+ dfn.merge!(
43
+ :instance => lambda { |fragment, *| fragment }, # use model's nested property for syncing.
44
+ # FIXME: do we allow options for #sync for nested forms?
45
+ :deserialize => lambda { |object, *| model = object.sync!({}) } # sync! returns the synced model.
46
+ # representable's :setter will do collection=([..]) or property=(..) for us on the model.
59
47
  )
60
48
  end
61
-
62
- super
63
49
  end
64
50
  end
65
51
 
52
+ # This representer inherits from sync_representer and add functionality on top of that.
53
+ # It allows running custom dynamic blocks for properties when syncing.
54
+ def dynamic_sync_representer
55
+ self.class.representer(:dynamic_sync, superclass: sync_representer, :all => true) do |dfn|
56
+ next unless setter = dfn[:sync]
66
57
 
67
- def sync_models(options={})
68
- sync!(options)
69
- end
70
- alias_method :sync, :sync_models
71
-
72
- # reading from fields allows using readers in form for presentation
73
- # and writers still pass to fields in #validate????
74
- def sync!(options) # semi-public.
75
- options = Reform::Representer::Options[options.merge(:form => self)] # options local for this form, only.
76
-
77
- input = sync_hash(options)
78
- # if aliased_model was a proper Twin, we could do changed? stuff there.
79
- # setter_module = Class.new(self.class.representer_class)
80
- # setter_module.send :include, Setter
81
-
82
- options.delete(:exclude) # TODO: can we use 2 options?
58
+ setter_proc = lambda do |value, options|
59
+ if options.binding[:sync] == true # sync: true will call the runtime lambda from the options hash.
60
+ options.user_options[options.binding.name.to_sym].call(value, options)
61
+ next
62
+ end
83
63
 
84
- mapper.new(aliased_model).extend(Writer).extend(Setter).from_hash(input, options) # sync properties to Song.
64
+ # evaluate the :sync block in form context (should we do that everywhere?).
65
+ options.user_options[:form].instance_exec(value, options, &setter)
66
+ end
85
67
 
86
- model
68
+ dfn.merge!(:setter => setter_proc)
69
+ end
87
70
  end
88
71
 
89
- private
72
+
90
73
  # API: semi-public.
91
74
  module SyncHash
92
75
  # This hash goes into the Writer that writes properties back to the model. It only contains "writeable" attributes.
93
76
  def sync_hash(options)
94
- input_representer = mapper.new(fields).extend(InputRepresenter)
95
- input_representer.to_hash(options)
77
+ input_representer.new(fields).to_hash(options)
96
78
  end
97
79
  end
98
80
  include SyncHash
99
81
 
100
82
 
101
- # Excludes :virtual properties from #sync in this form.
102
- module ReadOnly
83
+ # Excludes :virtual and readonly properties from #sync in this form.
84
+ module Writeable
103
85
  def sync_hash(options)
104
- readonly_fields = mapper.fields { |dfn| dfn[:virtual] }
86
+ readonly_fields = mapper.fields { |dfn| dfn[:_writeable] == false }
105
87
 
106
88
  options.exclude!(readonly_fields.map(&:to_sym))
107
89
 
108
90
  super
109
91
  end
110
92
  end
111
- include ReadOnly
93
+ include Writeable
112
94
 
113
95
 
114
96
  # This will skip unchanged properties in #sync. To use this for all nested form do as follows.
@@ -1,48 +1,5 @@
1
1
  # Mechanics for writing to forms in #validate.
2
2
  module Reform::Form::Validate
3
- module Update
4
- # IDEA: what if Populate was a Decorator that simply knows how to setup the Form object graph, nothing more? That would decouple
5
- # the population from the validation (good and bad as less customizable).
6
-
7
- # Go through all nested forms and call form.update!(hash).
8
- def from_hash(*)
9
- nested_forms do |attr|
10
- attr.merge!(
11
- # set parse_strategy: sync> # DISCUSS: that kills the :setter directive, which usually sucks. at least document this in :populator.
12
- :collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
13
- :parse_strategy => :sync, # just use nested objects as they are.
14
-
15
- # :getter grabs nested forms directly from fields bypassing the reader method which could possibly be overridden for presentation.
16
- :getter => lambda { |options| fields.send(options.binding.name) },
17
- :deserialize => lambda { |object, params, args| object.update!(params) },
18
- )
19
-
20
- # TODO: :populator now is just an alias for :instance. handle in ::property.
21
- attr.merge!(:instance => attr[:populator]) if attr[:populator]
22
-
23
- attr.merge!(:instance => Populator::PopulateIfEmpty.new) if attr[:populate_if_empty]
24
- end
25
-
26
- # FIXME: solve this with a dedicated Populate Decorator per Form.
27
- representable_attrs.each do |attr|
28
- attr.merge!(:parse_filter => Representable::Coercion::Coercer.new(attr[:coercion_type])) if attr[:coercion_type]
29
-
30
- attr.merge!(:skip_if => Skip::AllBlank.new) if attr[:skip_if] == :all_blank
31
- attr.merge!(:skip_parse => attr[:skip_if]) if attr[:skip_if]
32
- end
33
-
34
-
35
- representable_attrs.each do |attr|
36
- next if attr[:form]
37
-
38
- attr.merge!(:parse_filter => Changed.new)
39
- end
40
-
41
- super
42
- end
43
- end
44
-
45
-
46
3
  module Populator
47
4
  # This might change soon (e.g. moved into disposable).
48
5
  class PopulateIfEmpty
@@ -130,10 +87,46 @@ private
130
87
  def deserialize!(params)
131
88
  # using self here will call the form's setters like title= which might be overridden.
132
89
  # from_hash(params, parent_form: self)
133
- mapper.new(self).extend(Update).send(deserialize_method, params, :parent_form => self)
90
+ # Go through all nested forms and call form.update!(hash).
91
+ populate_representer.new(self).send(deserialize_method, params, :parent_form => self)
134
92
  end
135
93
 
136
94
  def deserialize_method
137
95
  :from_hash
138
96
  end
97
+
98
+ # IDEA: what if Populate was a Decorator that simply knows how to setup the Form object graph, nothing more? That would decouple
99
+ # the population from the validation (good and bad as less customizable).
100
+
101
+ # Don't get scared by this method. All this does is create a new representer class for this form.
102
+ # It then configures each property so the population of the form can happen in #validate.
103
+ # A lot of this code is simply renaming from Reform's API to representable's. # FIXME: unify that?
104
+ def populate_representer
105
+ self.class.representer(:populate, :all => true) do |dfn|
106
+ if dfn[:form]
107
+ dfn.merge!(
108
+ # set parse_strategy: sync> # DISCUSS: that kills the :setter directive, which usually sucks. at least document this in :populator.
109
+ :collection => dfn[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in dfn YET.
110
+ :parse_strategy => :sync, # just use nested objects as they are.
111
+
112
+ # :getter grabs nested forms directly from fields bypassing the reader method which could possibly be overridden for presentation.
113
+ :getter => lambda { |options| fields.send(options.binding.name) },
114
+ :deserialize => lambda { |object, params, args| object.update!(params) },
115
+ )
116
+
117
+ # TODO: :populator now is just an alias for :instance. handle in ::property.
118
+ dfn.merge!(:instance => dfn[:populator]) if dfn[:populator]
119
+
120
+ dfn.merge!(:instance => Populator::PopulateIfEmpty.new) if dfn[:populate_if_empty]
121
+ end
122
+
123
+
124
+ dfn.merge!(:parse_filter => Representable::Coercion::Coercer.new(dfn[:coercion_type])) if dfn[:coercion_type]
125
+
126
+ dfn.merge!(:skip_if => Skip::AllBlank.new) if dfn[:skip_if] == :all_blank
127
+ dfn.merge!(:skip_parse => dfn[:skip_if]) if dfn[:skip_if]
128
+
129
+ dfn.merge!(:parse_filter => Changed.new) unless dfn[:form] # TODO: make changed? work for nested forms.
130
+ end
131
+ end
139
132
  end
@@ -38,10 +38,12 @@ module Reform
38
38
  representable_attrs.find_all(&block).map(&:name)
39
39
  end
40
40
 
41
- def nested_forms(&block)
42
- clone_config!.
43
- find_all { |attr| attr[:form] }.
44
- each(&block)
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
45
47
  end
46
48
 
47
49
  def self.for(options)
@@ -59,12 +61,6 @@ module Reform
59
61
  end
60
62
 
61
63
  private
62
- def clone_config!
63
- # TODO: representable_attrs.clone! which does exactly what's done below.
64
- attrs = Representable::Config.new
65
- attrs.inherit!(representable_attrs) # since in every use case we modify Config we clone.
66
- @representable_attrs = attrs
67
- end
68
64
 
69
65
  # Inline forms always get saved in :extend.
70
66
  def self.build_inline(base, features, name, options, &block)
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "1.2.0.beta2"
2
+ VERSION = "1.2.1"
3
3
  end
@@ -105,6 +105,7 @@ end
105
105
 
106
106
  class PopulateWithActiveRecordTest < MiniTest::Spec
107
107
  class AlbumForm < Reform::Form
108
+
108
109
  property :title
109
110
 
110
111
  collection :songs, :populate_if_empty => Song do
@@ -188,6 +189,73 @@ class PopulateWithActiveRecordTest < MiniTest::Spec
188
189
  album.songs[1].title.must_equal "Check For A Pulse"
189
190
  album.songs[1].persisted?.must_equal true # TODO: with << strategy, this shouldn't be saved.
190
191
  end
192
+
193
+ describe 'using nested_models_attributes to modify nested collection' do
194
+ class ActiveModelAlbumForm < Reform::Form
195
+ include Reform::Form::ActiveModel
196
+ include Reform::Form::ActiveModel::FormBuilderMethods
197
+
198
+ property :title
199
+
200
+ collection :songs, :populate_if_empty => Song do
201
+ property :title
202
+ end
203
+ end
204
+
205
+ let (:album) { Album.create(:title => 'Greatest Hits') }
206
+ let (:form) { ActiveModelAlbumForm.new(album) }
207
+
208
+ it do
209
+ form.validate('songs_attributes' => {'0' => {'title' => 'Tango'}})
210
+
211
+ # form populated.
212
+ form.songs.size.must_equal 1
213
+ form.songs[0].model.must_be_kind_of Song
214
+ form.songs[0].title.must_equal 'Tango'
215
+
216
+ # model NOT populated.
217
+ album.songs.must_equal []
218
+
219
+ form.save
220
+
221
+ # nested model persisted.
222
+ first_song = album.songs[0]
223
+ assert first_song.id > 0
224
+
225
+ # form populated.
226
+ form.songs.size.must_equal 1
227
+
228
+ # model also populated.
229
+ album.songs.size.must_equal 1
230
+ album.songs[0].title.must_equal 'Tango'
231
+
232
+ form.validate('songs_attributes' => {'0' => {'id' => first_song.id, 'title' => 'Tango nuevo'}, '1' => {'title' => 'Waltz'}})
233
+
234
+ # form populated.
235
+ form.songs.size.must_equal 2
236
+ form.songs[0].model.must_be_kind_of Song
237
+ form.songs[1].model.must_be_kind_of Song
238
+ form.songs[0].title.must_equal 'Tango nuevo'
239
+ form.songs[1].title.must_equal 'Waltz'
240
+
241
+ # model NOT populated.
242
+ album.songs.size.must_equal 1
243
+ album.songs[0].title.must_equal 'Tango'
244
+
245
+ form.save
246
+
247
+ # form populated.
248
+ form.songs.size.must_equal 2
249
+
250
+ # model also populated.
251
+ album.songs.size.must_equal 2
252
+ album.songs[0].id.must_equal first_song.id
253
+ album.songs[0].persisted?.must_equal true
254
+ album.songs[1].persisted?.must_equal true
255
+ album.songs[0].title.must_equal 'Tango nuevo'
256
+ album.songs[1].title.must_equal 'Waltz'
257
+ end
258
+ end
191
259
  end
192
260
 
193
261
  # it do