dm-accepts_nested_attributes_for 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +970 -0
- data/Gemfile +84 -0
- data/LICENSE +20 -0
- data/README.textile +94 -0
- data/Rakefile +25 -0
- data/TODO +6 -0
- data/VERSION +1 -0
- data/dm-accepts_nested_attributes.gemspec +114 -0
- data/lib/dm-accepts_nested_attributes.rb +13 -0
- data/lib/dm-accepts_nested_attributes/model.rb +144 -0
- data/lib/dm-accepts_nested_attributes/relationship.rb +82 -0
- data/lib/dm-accepts_nested_attributes/resource.rb +382 -0
- data/lib/dm-accepts_nested_attributes/version.rb +7 -0
- data/spec/accepts_nested_attributes_for_spec.rb +408 -0
- data/spec/assign_nested_attributes_for_spec.rb +101 -0
- data/spec/comb/1-1_disjoint_spec.rb +67 -0
- data/spec/comb/1-1_overlapping_spec.rb +66 -0
- data/spec/comb/1-1_subset_spec.rb +65 -0
- data/spec/comb/1-1_superset_spec.rb +67 -0
- data/spec/comb/1-m_disjoint_spec.rb +71 -0
- data/spec/comb/1-m_overlapping_spec.rb +70 -0
- data/spec/comb/1-m_subset_spec.rb +65 -0
- data/spec/comb/1-m_superset_spec.rb +71 -0
- data/spec/comb/m-1_disjoint_spec.rb +71 -0
- data/spec/comb/m-1_overlapping_spec.rb +70 -0
- data/spec/comb/m-1_subset_spec.rb +65 -0
- data/spec/comb/m-1_superset_spec.rb +71 -0
- data/spec/comb/n-m_composite_spec.rb +141 -0
- data/spec/comb/n-m_surrogate_spec.rb +154 -0
- data/spec/many_to_many_composite_spec.rb +120 -0
- data/spec/many_to_many_spec.rb +129 -0
- data/spec/many_to_one_composite_spec.rb +120 -0
- data/spec/many_to_one_spec.rb +101 -0
- data/spec/one_to_many_composite_spec.rb +120 -0
- data/spec/one_to_many_spec.rb +100 -0
- data/spec/one_to_one_composite_spec.rb +150 -0
- data/spec/one_to_one_spec.rb +115 -0
- data/spec/rcov.opts +6 -0
- data/spec/resource_spec.rb +65 -0
- data/spec/shared/many_to_many_composite_spec.rb +149 -0
- data/spec/shared/many_to_many_spec.rb +146 -0
- data/spec/shared/many_to_one_composite_spec.rb +160 -0
- data/spec/shared/many_to_one_spec.rb +130 -0
- data/spec/shared/one_to_many_composite_spec.rb +159 -0
- data/spec/shared/one_to_many_spec.rb +107 -0
- data/spec/shared/one_to_one_composite_spec.rb +114 -0
- data/spec/shared/one_to_one_spec.rb +111 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/update_dirty_spec.rb +113 -0
- data/spec/update_multiple_spec.rb +79 -0
- data/tasks/changelog.rake +20 -0
- data/tasks/ci.rake +1 -0
- data/tasks/local_gemfile.rake +18 -0
- data/tasks/spec.rake +22 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +216 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
# Extensions for {DataMapper::Associations::Relationship}.
|
5
|
+
module Relationship
|
6
|
+
# Extracts the primary key values necessary to retrieve or update a nested
|
7
|
+
# model when using {Model#accepts_nested_attributes_for}. Values are taken from
|
8
|
+
# the specified model and attribute hash with the former having priority.
|
9
|
+
# Values for properties in the primary key that are *not* included in the
|
10
|
+
# foreign key must be specified in the attributes hash.
|
11
|
+
#
|
12
|
+
# @param model [DataMapper::Model]
|
13
|
+
# The model that accepts nested attributes.
|
14
|
+
#
|
15
|
+
# @param attributes [Hash]
|
16
|
+
# The attributes assigned to the nested attribute setter on the
|
17
|
+
# +model+.
|
18
|
+
#
|
19
|
+
# @return [Array]
|
20
|
+
def extract_keys_for_nested_attributes(model, attributes)
|
21
|
+
raise NotImplementedError, "extract_keys must be overridden in a derived class"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Extensions for {DataMapper::Associations::ManyToMany::Relationship}.
|
26
|
+
module ManyToMany
|
27
|
+
def extract_keys_for_nested_attributes(model, attributes)
|
28
|
+
keys = self.child_key.map do |key|
|
29
|
+
attributes[key.name]
|
30
|
+
end
|
31
|
+
|
32
|
+
keys.any? { |key| DataMapper::Ext.blank?(key) } ? nil : keys
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Extensions for {DataMapper::Associations::OneToMany::Relationship}.
|
37
|
+
module OneToMany
|
38
|
+
def extract_keys_for_nested_attributes(model, attributes)
|
39
|
+
keys = self.child_model.key.to_enum(:each_with_index).map do |key, idx|
|
40
|
+
if parent_idx = self.child_key.to_a.index(key)
|
41
|
+
model[self.parent_key.to_a.at(parent_idx).name]
|
42
|
+
else
|
43
|
+
attributes[key.name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
keys.any? { |key| DataMapper::Ext.blank?(key) } ? nil : keys
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Extensions for {DataMapper::Associations::ManyToOne::Relationship}.
|
52
|
+
module ManyToOne
|
53
|
+
def extract_keys_for_nested_attributes(model, attributes)
|
54
|
+
keys = self.parent_model.key.to_enum(:each_with_index).map do |key, idx|
|
55
|
+
if child_idx = self.parent_key.to_a.index(key)
|
56
|
+
model[self.child_key.to_a.at(child_idx).name]
|
57
|
+
else
|
58
|
+
attributes[key.name]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
keys.any? { |key| DataMapper::Ext.blank?(key) } ? nil : keys
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Extensions for {DataMapper::Associations::OneToOne::Relationship}.
|
67
|
+
module OneToOne
|
68
|
+
def extract_keys_for_nested_attributes(model, attributes)
|
69
|
+
keys = self.child_model.key.to_enum(:each_with_index).map do |key, idx|
|
70
|
+
if parent_idx = self.child_key.to_a.index(key)
|
71
|
+
model[self.parent_key.to_a.at(parent_idx).name]
|
72
|
+
else
|
73
|
+
attributes[key.name]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
keys.any? { |key| DataMapper::Ext.blank?(key) } ? nil : keys
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
##
|
5
|
+
# Extensions and customizations for a {DataMapper::Resource}
|
6
|
+
# that are needed if the {DataMapper::Resource} wants to
|
7
|
+
# accept nested attributes for any given relationship.
|
8
|
+
# Basically, this module provides functionality that allows
|
9
|
+
# either assignment or marking for destruction of related parent
|
10
|
+
# and child associations, based on the given attributes and what
|
11
|
+
# kind of relationship should be altered.
|
12
|
+
module Resource
|
13
|
+
# Truthy values for the +:_delete+ flag.
|
14
|
+
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
15
|
+
|
16
|
+
##
|
17
|
+
# Can be used to remove ambiguities from the passed attributes.
|
18
|
+
# Consider a situation with a belongs_to association where both a valid value
|
19
|
+
# for the foreign_key attribute *and* nested_attributes for a new record are
|
20
|
+
# present (i.e. item_type_id and item_type_attributes are present).
|
21
|
+
# Also see http://is.gd/sz2d on the rails-core ml for a discussion on this.
|
22
|
+
# The basic idea is, that there should be a well defined behavior for what
|
23
|
+
# exactly happens when such a situation occurs. I'm currently in favor for
|
24
|
+
# using the foreign_key if it is present, but this probably needs more thinking.
|
25
|
+
# For now, this method basically is a no-op, but at least it provides a hook where
|
26
|
+
# everyone can perform it's own sanitization by overwriting this method.
|
27
|
+
#
|
28
|
+
# @param attributes [Hash]
|
29
|
+
# The attributes to sanitize.
|
30
|
+
#
|
31
|
+
# @return [Hash]
|
32
|
+
# The sanitized attributes.
|
33
|
+
#
|
34
|
+
def sanitize_nested_attributes(attributes)
|
35
|
+
attributes # noop
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Saves the resource and destroys nested resources marked for destruction.
|
40
|
+
def save(*)
|
41
|
+
saved = super
|
42
|
+
remove_destroyables
|
43
|
+
saved
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
##
|
49
|
+
# Attribute hash keys that are excluded when creating a nested resource.
|
50
|
+
# Excluded attributes include +:_delete+, a special value used to mark a
|
51
|
+
# resource for destruction.
|
52
|
+
#
|
53
|
+
# @return [Array<Symbol>] Excluded attribute names.
|
54
|
+
def uncreatable_keys
|
55
|
+
[:_delete]
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Attribute hash keys that are excluded when updating a nested resource.
|
60
|
+
# Excluded attributes include the model key and :_delete, a special value
|
61
|
+
# used to mark a resource for destruction.
|
62
|
+
#
|
63
|
+
# @return [Array<Symbol>] Excluded attribute names.
|
64
|
+
def unupdatable_keys
|
65
|
+
model.key.to_a.map { |property| property.name } << :_delete
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
##
|
70
|
+
# Assigns the given attributes to the resource association.
|
71
|
+
#
|
72
|
+
# If the given attributes include the primary key values that match the
|
73
|
+
# existing record’s keys, then the existing record will be modified.
|
74
|
+
# Otherwise a new record will be built.
|
75
|
+
#
|
76
|
+
# If the given attributes include matching primary key values _and_ a
|
77
|
+
# <tt>:_delete</tt> key set to a truthy value, then the existing record
|
78
|
+
# will be marked for destruction.
|
79
|
+
#
|
80
|
+
# The names of the primary key values required depend on the configuration
|
81
|
+
# of the association. It is not necessary to specify values for attributes
|
82
|
+
# that exist on this resource as they are inferred.
|
83
|
+
#
|
84
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
85
|
+
# The relationship backing the association.
|
86
|
+
# Assignment will happen on the target end of the relationship
|
87
|
+
#
|
88
|
+
# @param attributes [Hash{Symbol => Object}]
|
89
|
+
# The attributes to assign to the relationship's target end.
|
90
|
+
# All attributes except {#uncreatable_keys} (for new resources) and
|
91
|
+
# {#unupdatable_keys} (when updating an existing resource) will be
|
92
|
+
# assigned.
|
93
|
+
#
|
94
|
+
# @return [void]
|
95
|
+
def assign_nested_attributes_for_related_resource(relationship, attributes)
|
96
|
+
assert_kind_of 'attributes', attributes, Hash
|
97
|
+
|
98
|
+
if keys = extract_keys(relationship, attributes)
|
99
|
+
existing_record = relationship.get(self)
|
100
|
+
if existing_record && existing_record.key == keys
|
101
|
+
update_or_mark_as_destroyable(relationship, existing_record, attributes)
|
102
|
+
return
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
return if reject_new_record?(relationship, attributes)
|
107
|
+
|
108
|
+
attributes = DataMapper::Ext::Hash.except(attributes, *uncreatable_keys)
|
109
|
+
new_record = relationship.target_model.new(attributes)
|
110
|
+
relationship.set(self, new_record)
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Assigns the given attributes to the collection association.
|
115
|
+
#
|
116
|
+
# Hashes with primary key values matching an existing associated record
|
117
|
+
# will update that record. Hashes without primary key values (or only
|
118
|
+
# values for a partial primary key), or if no existing associated record
|
119
|
+
# exists, will build a new record for the association. Hashes with
|
120
|
+
# matching primary key values and a <tt>:_delete</tt> key set to a truthy
|
121
|
+
# value will mark the matched record for destruction.
|
122
|
+
#
|
123
|
+
# The names of the primary key values required depend on the configuration
|
124
|
+
# of the association. It is not necessary to specify values for attributes
|
125
|
+
# that exist on this resource as they are inferred.
|
126
|
+
#
|
127
|
+
# For example:
|
128
|
+
#
|
129
|
+
# assign_nested_attributes_for_collection_association(:people, {
|
130
|
+
# '1' => { :id => '1', :name => 'Peter' },
|
131
|
+
# '2' => { :name => 'John' },
|
132
|
+
# '3' => { :id => '2', :_delete => true }
|
133
|
+
# })
|
134
|
+
#
|
135
|
+
# Will update the name of the Person with ID 1, build a new associated
|
136
|
+
# person with the name 'John', and mark the associatied Person with ID 2
|
137
|
+
# for destruction.
|
138
|
+
#
|
139
|
+
# assign_nested_attributes_for_collection_association(:people, {
|
140
|
+
# '1' => { :person_id => '1', :audit_id => 2, :name => 'Peter' },
|
141
|
+
# '2' => { :audit_id => 2, :name => 'John' },
|
142
|
+
# '3' => { :person_id => '2', :audit_id => 3, :_delete => true }
|
143
|
+
# })
|
144
|
+
#
|
145
|
+
# Will update the name of the Person with `(person_id, audit_id) = (1, 2)`,
|
146
|
+
# build a new associated person with the name 'John', and mark the
|
147
|
+
# associatied Person with key `(2, 3)` for destruction.
|
148
|
+
#
|
149
|
+
# Also accepts an Array of attribute hashes:
|
150
|
+
#
|
151
|
+
# assign_nested_attributes_for_collection_association(:people, [
|
152
|
+
# { :id => '1', :name => 'Peter' },
|
153
|
+
# { :name => 'John' },
|
154
|
+
# { :id => '2', :_delete => true }
|
155
|
+
# ])
|
156
|
+
#
|
157
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
158
|
+
# The relationship backing the association.
|
159
|
+
# Assignment will happen on the target end of the relationship
|
160
|
+
#
|
161
|
+
# @param attributes_collection [Hash{Integer=>Hash}, Array<Hash>]
|
162
|
+
# The attributes to assign to the relationship's target end.
|
163
|
+
# All attributes except {#uncreatable_keys} (for new resources) and
|
164
|
+
# {#unupdatable_keys} (when updating an existing resource) will be
|
165
|
+
# assigned.
|
166
|
+
#
|
167
|
+
# @return [void]
|
168
|
+
def assign_nested_attributes_for_related_collection(relationship, attributes_collection)
|
169
|
+
assert_hash_or_array_of_hashes("attributes_collection", attributes_collection)
|
170
|
+
|
171
|
+
normalize_attributes_collection(attributes_collection).each do |attributes|
|
172
|
+
if keys = extract_keys(relationship, attributes)
|
173
|
+
collection = relationship.get(self)
|
174
|
+
if existing_record = collection.get(*keys)
|
175
|
+
update_or_mark_as_destroyable(relationship, existing_record, attributes)
|
176
|
+
next
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
next if reject_new_record?(relationship, attributes)
|
181
|
+
|
182
|
+
attributes = DataMapper::Ext::Hash.except(attributes, *uncreatable_keys)
|
183
|
+
relationship.get(self).new(attributes)
|
184
|
+
end
|
185
|
+
|
186
|
+
nil
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Extracts the primary key values necessary to retrieve or update a nested
|
191
|
+
# model when using +accepts_nested_attributes_for+. Values are taken from
|
192
|
+
# this model instance and attribute hash with the former having priority.
|
193
|
+
# Values for properties in the primary key that are *not* included in the
|
194
|
+
# foreign key must be specified in the attributes hash.
|
195
|
+
#
|
196
|
+
# @param relationship [DataMapper::Association::Relationship]
|
197
|
+
# The relationship backing the association.
|
198
|
+
#
|
199
|
+
# @param attributes [Hash{Symbol => Object}]
|
200
|
+
# The attributes assigned to the nested attribute setter on the
|
201
|
+
# +model+.
|
202
|
+
#
|
203
|
+
# @return [Array]
|
204
|
+
def extract_keys(relationship, attributes)
|
205
|
+
relationship.extract_keys_for_nested_attributes(self, attributes)
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# Updates a record with the +attributes+ or marks it for destruction if
|
210
|
+
# the +:allow_destroy+ option is +true+ and {#has_delete_flag?} returns
|
211
|
+
# +true+.
|
212
|
+
#
|
213
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
214
|
+
# The relationship backing the association.
|
215
|
+
# Assignment will happen on the target end of the relationship
|
216
|
+
#
|
217
|
+
# @param attributes [Hash{Symbol => Object}]
|
218
|
+
# The attributes to assign to the relationship's target end.
|
219
|
+
# All attributes except {#unupdatable_keys} will be assigned.
|
220
|
+
#
|
221
|
+
# @return [void]
|
222
|
+
def update_or_mark_as_destroyable(relationship, resource, attributes)
|
223
|
+
allow_destroy = self.class.options_for_nested_attributes[relationship.name][:allow_destroy]
|
224
|
+
if has_delete_flag?(attributes) && allow_destroy
|
225
|
+
if relationship.is_a?(DataMapper::Associations::ManyToMany::Relationship)
|
226
|
+
intermediaries = relationship.through.get(self).all(relationship.via => resource)
|
227
|
+
intermediaries.each { |intermediate| destroyables << intermediate }
|
228
|
+
end
|
229
|
+
destroyables << resource
|
230
|
+
else
|
231
|
+
assert_nested_update_clean_only(resource)
|
232
|
+
resource.attributes = DataMapper::Ext::Hash.except(attributes, *unupdatable_keys)
|
233
|
+
resource.save
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Determines whether the given attributes hash contains a truthy :_delete key.
|
239
|
+
#
|
240
|
+
# @param attributes [Hash{Symbol => Object}] The attributes to test.
|
241
|
+
#
|
242
|
+
# @return [Boolean]
|
243
|
+
# +true+ if attributes contains a truthy :_delete key.
|
244
|
+
#
|
245
|
+
# @see TRUE_VALUES
|
246
|
+
def has_delete_flag?(attributes)
|
247
|
+
value = attributes[:_delete]
|
248
|
+
if value.is_a?(String) && value !~ /\S/
|
249
|
+
nil
|
250
|
+
else
|
251
|
+
TRUE_VALUES.include?(value)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
##
|
256
|
+
# Determines if a new record should be built with the given attributes.
|
257
|
+
# Rejects a new record if {#has_delete_flag?} returns +true+ for the given
|
258
|
+
# attributes, or if a +:reject_if+ guard exists for the passed relationship
|
259
|
+
# that evaluates to +true+.
|
260
|
+
#
|
261
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
262
|
+
# The relationship backing the association.
|
263
|
+
#
|
264
|
+
# @param attributes [Hash{Symbol => Object}]
|
265
|
+
# The attributes to test with {#has_delete_flag?}.
|
266
|
+
#
|
267
|
+
# @return [Boolean]
|
268
|
+
# +true+ if the given attributes will be rejected.
|
269
|
+
def reject_new_record?(relationship, attributes)
|
270
|
+
guard = self.class.options_for_nested_attributes[relationship.name][:reject_if]
|
271
|
+
return false if guard.nil? # if relationship guard is nil, nothing will be rejected
|
272
|
+
has_delete_flag?(attributes) || evaluate_reject_new_record_guard(guard, attributes)
|
273
|
+
end
|
274
|
+
|
275
|
+
##
|
276
|
+
# Evaluates the given guard by calling it with the given attributes.
|
277
|
+
#
|
278
|
+
# @param [Symbol, String, #call] guard
|
279
|
+
# An instance method name or an object that respond_to?(:call), which
|
280
|
+
# would stop a new record from being created, if it evaluates to true.
|
281
|
+
#
|
282
|
+
# @param [Hash{Symbol => Object}] attributes
|
283
|
+
# The attributes to pass to the guard for evaluating if it should reject
|
284
|
+
# the creation of a new resource
|
285
|
+
#
|
286
|
+
# @raise [ArgumentError]
|
287
|
+
# If the given guard doesn't match [Symbol, String, #call]
|
288
|
+
#
|
289
|
+
# @return [Boolean]
|
290
|
+
# The value returned by evaluating the guard
|
291
|
+
def evaluate_reject_new_record_guard(guard, attributes)
|
292
|
+
if guard.is_a?(Symbol) || guard.is_a?(String)
|
293
|
+
send(guard, attributes)
|
294
|
+
elsif guard.respond_to?(:call)
|
295
|
+
guard.call(attributes)
|
296
|
+
else
|
297
|
+
# never reached when called from inside the plugin
|
298
|
+
raise ArgumentError, "guard must be a Symbol, a String, or respond_to?(:call)"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
##
|
303
|
+
# Raises an exception if the specified resource is dirty or has dirty
|
304
|
+
# children.
|
305
|
+
#
|
306
|
+
# @param [DataMapper::Resource] resource
|
307
|
+
# The resource to check.
|
308
|
+
#
|
309
|
+
# @return [void]
|
310
|
+
#
|
311
|
+
# @raise [UpdateConflictError]
|
312
|
+
# If the resource is dirty.
|
313
|
+
#
|
314
|
+
# @api private
|
315
|
+
def assert_nested_update_clean_only(resource)
|
316
|
+
if resource.send(:dirty_self?) || resource.send(:dirty_children?)
|
317
|
+
raise UpdateConflictError, "#{model}#update cannot be called on a #{new? ? 'new' : 'dirty'} nested resource"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
##
|
322
|
+
# Asserts that the specified parameter value is a Hash of Hashes, or an
|
323
|
+
# Array of Hashes and raises an ArgumentError if value does not conform.
|
324
|
+
#
|
325
|
+
# @param param_name [String]
|
326
|
+
# The parameter name included in the raised ArgumentError.
|
327
|
+
#
|
328
|
+
# @param value
|
329
|
+
# The value to check.
|
330
|
+
#
|
331
|
+
# @return [void]
|
332
|
+
def assert_hash_or_array_of_hashes(param_name, value)
|
333
|
+
if value.is_a?(Hash)
|
334
|
+
unless value.values.all? { |a| a.is_a?(Hash) }
|
335
|
+
raise ArgumentError,
|
336
|
+
"+#{param_name}+ should be a Hash of Hashes or Array " +
|
337
|
+
"of Hashes, but was a Hash with #{value.values.map { |a| a.class }.uniq.inspect}"
|
338
|
+
end
|
339
|
+
elsif value.is_a?(Array)
|
340
|
+
unless value.all? { |a| a.is_a?(Hash) }
|
341
|
+
raise ArgumentError,
|
342
|
+
"+#{param_name}+ should be a Hash of Hashes or Array " +
|
343
|
+
"of Hashes, but was an Array with #{value.map { |a| a.class }.uniq.inspect}"
|
344
|
+
end
|
345
|
+
else
|
346
|
+
raise ArgumentError,
|
347
|
+
"+#{param_name}+ should be a Hash of Hashes or Array of " +
|
348
|
+
"Hashes, but was #{value.class}"
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
##
|
353
|
+
# Make sure to return a collection of attribute hashes.
|
354
|
+
# If passed an attributes hash, map it to its attributes.
|
355
|
+
#
|
356
|
+
# @param attributes [Hash, #each]
|
357
|
+
# An attributes hash or a collection of attribute hashes.
|
358
|
+
#
|
359
|
+
# @return [#each]
|
360
|
+
# A collection of attribute hashes.
|
361
|
+
def normalize_attributes_collection(attributes)
|
362
|
+
if attributes.is_a?(Hash)
|
363
|
+
attributes.map { |_, attributes| attributes }
|
364
|
+
else
|
365
|
+
attributes
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
|
370
|
+
def destroyables
|
371
|
+
@destroyables ||= []
|
372
|
+
end
|
373
|
+
|
374
|
+
def remove_destroyables
|
375
|
+
destroyables.each { |r| r.destroy if r.saved? }
|
376
|
+
@destroyables.clear
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
end
|