dm-accepts_nested_attributes_for 1.2.0
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.
- 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
|