snusnu-dm-accepts_nested_attributes 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,12 +10,11 @@ lib/dm-accepts_nested_attributes.rb
10
10
  lib/dm-accepts_nested_attributes/error_collecting.rb
11
11
  lib/dm-accepts_nested_attributes/model.rb
12
12
  lib/dm-accepts_nested_attributes/resource.rb
13
- lib/dm-accepts_nested_attributes/save.rb
14
13
  lib/dm-accepts_nested_attributes/transactional_save.rb
15
14
  lib/dm-accepts_nested_attributes/version.rb
16
15
  spec/spec.opts
17
16
  spec/spec_helper.rb
18
- spec/lib/rspec_tmbundle_support.rb
17
+ spec/lib/constraint_support.rb
19
18
  spec/fixtures/person.rb
20
19
  spec/fixtures/profile.rb
21
20
  spec/fixtures/project.rb
@@ -2,66 +2,7 @@ h2. dm-accepts_nested_attributes
2
2
 
3
3
  A DataMapper plugin that allows nested model attribute assignment like activerecord does.
4
4
 
5
- At the end of this file, you can see a list of all current integration specs.
6
-
7
- For more information on the progress, have a look at this README and also at
8
- "this article":http://sick.snusnu.info/2009/04/08/dm-accepts_nested_attributes/ on my blog, where I will try to comment on the
9
- development (problems).
10
-
11
- h2. Current limitations
12
-
13
- Interaction with @dm-validations@ is actually possible but not very well specced atm. I added @not null@
14
- constraints to all spec fixtures for now, but no other custom validations. All specs still pass. However,
15
- as long as I'm not decided on where to put the specs for interaction with @dm-validations@ (most probably
16
- inside @dm-validations@ would be the right place for these), I won't write many more specs for these scenarios,
17
- since it's very hard to decide where to stop, once I start writing some.
18
-
19
- Currently, the creation of the record together with all its join models, is not handled inside a transaction.
20
- This must definitely change! As soon as I find out why my initial experiments with transactions consistently
21
- yielded _no such table errors_ while migrating the specsuite (see "this pastie":http://pastie.org/446060), I
22
- will do my best to add this feature.
23
-
24
- h2. TODO
25
-
26
- * use transactions
27
- * update README to include more complete usecases
28
- * specs for custom validations (dm-validations in general)
29
- * specs for adding errors to the parent objects
30
- * reorganize specs and fix bugs
31
- * Adapt to datamapper/next
32
-
33
- h2. Implementation details
34
-
35
- This section mainly serves as a place for me to take notes during development.
36
-
37
- h3. Why isn't this implemented as options on association declarations?
38
-
39
- * I somehow like the declarative style of @accepts_nested_attributes_for@ better. it jumps out immediately.
40
- * The API for datamapper and activerecord is the same.
41
- * association definitions can already get quite long and "unreadable". chances are you overlook it!
42
-
43
- h3. Why doesn't accepts_nested_attributes_for take more than one association_name?
44
-
45
- While writing the unit specs for this method, I realised that there are way too many ways to call this
46
- method, which makes it "hard" to spec all possible calls. That's why I started to list Pros and Cons, and
47
- decided to support only one @association_name@ per call, at least for now.
48
-
49
- h4. Pros
50
-
51
- * less complex code
52
- * fewer ways to call the method (simpler to understand, easier to spec)
53
- * easier to read (nr of calls == nr of accessible associations, this could be seen as a con also)
54
- * easier (and more extensible) option handling
55
- ** options don't implicitly apply to _all_ associations (could be seen as a con also?)
56
- ** options can explicitly be applied to _only the desired_ associations
57
- ** reject_if option maybe often makes more sense on exactly _one_ associaton (maybe not?)
58
- * no question what happens if the association_name is invalid (the whole call is invalid)
59
- ** with at least one _invalid_ association_name, what happens for the other _valid_ ones?
60
-
61
- h4. Cons
62
-
63
- * needs more method calls (overhead should be minimal)
64
- * options that apply to more than one attribute need to be duplicated (maybe a Pro because of readability)
5
+ Current documentation can always be found at "rdoc.info":http://rdoc.info/projects/snusnu/dm-accepts_nested_attributes
65
6
 
66
7
  h3. Examples
67
8
 
@@ -71,10 +12,6 @@ The following example illustrates the use of this plugin.
71
12
  <code>
72
13
  require "rubygems"
73
14
 
74
- gem 'dm-core', '0.9.11'
75
- gem 'dm-validations', '0.9.11'
76
- gem 'dm-accepts_nested_attributes', '0.0.1'
77
-
78
15
  require "dm-core"
79
16
  require "dm-validations"
80
17
  require "dm-accepts_nested_attributes"
@@ -89,9 +26,10 @@ class Person
89
26
  has 1, :profile
90
27
  has n, :project_memberships
91
28
  has n, :projects, :through => :project_memberships
29
+
92
30
  accepts_nested_attributes_for :profile
93
31
  accepts_nested_attributes_for :projects
94
-
32
+
95
33
  # adds the following instance methods
96
34
  # #profile_attributes
97
35
  # #projects_attributes
@@ -102,8 +40,9 @@ class Profile
102
40
  property :id, Serial
103
41
  property :person_id, Integer
104
42
  belongs_to :person
43
+
105
44
  accepts_nested_attributes_for :person
106
-
45
+
107
46
  # adds the following instance methods
108
47
  # #person_attributes
109
48
  end
@@ -114,6 +53,7 @@ class Project
114
53
  has n, :tasks
115
54
  has n, :project_memberships
116
55
  has n, :people, :through => :project_memberships
56
+
117
57
  accepts_nested_attributes_for :tasks
118
58
  accepts_nested_attributes_for :people
119
59
 
@@ -129,9 +69,6 @@ class ProjectMembership
129
69
  property :project_id, Integer
130
70
  belongs_to :person
131
71
  belongs_to :project
132
-
133
- # nothing added here
134
- # code only listed to provide complete example env
135
72
  end
136
73
 
137
74
  class Task
@@ -139,133 +76,30 @@ class Task
139
76
  property :id, Serial
140
77
  property :project_id, Integer
141
78
  belongs_to :project
142
-
143
- # nothing added here
144
- # code only listed to provide complete example env
145
79
  end
146
80
 
147
81
  DataMapper.auto_migrate!
148
82
  </code>
149
83
  </pre>
150
84
 
151
- h2. Current Integration Specs
152
-
153
- <pre>
154
- <code>
155
- DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person)
156
- - should allow to create a new person via Profile#person_attributes
157
- - should allow to update an existing person via Profile#person_attributes
158
- - should not allow to delete an existing person via Profile#person_attributes
159
-
160
- DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy => false)
161
- - should allow to create a new person via Profile#person_attributes
162
- - should allow to update an existing person via Profile#person_attributes
163
- - should not allow to delete an existing person via Profile#person_attributes
164
-
165
- DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy = true)
166
- - should allow to create a new person via Profile#person_attributes
167
- - should allow to update an existing person via Profile#person_attributes
168
- - should allow to delete an existing person via Profile#person_attributes
169
-
170
- DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => :foo
171
- - should allow to create a new person via Profile#person_attributes
172
- - should allow to update an existing person via Profile#person_attributes
173
- - should not allow to delete an existing person via Profile#person_attributes
174
-
175
- DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| true }
176
- - should not allow to create a new person via Profile#person_attributes
177
- - should not allow to delete an existing person via Profile#person_attributes
178
-
179
- DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| false }
180
- - should allow to create a new person via Profile#person_attributes
181
- - should allow to update an existing person via Profile#person_attributes
182
- - should not allow to delete an existing person via Profile#person_attributes
183
-
184
- DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile)
185
- - should allow to create a new profile via Person#profile_attributes
186
- - should allow to update an existing profile via Person#profile_attributes
187
- - should not allow to delete an existing profile via Person#profile_attributes
188
-
189
- DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => false)
190
- - should allow to create a new profile via Person#profile_attributes
191
- - should allow to update an existing profile via Person#profile_attributes
192
- - should not allow to delete an existing profile via Person#profile_attributes
193
-
194
- DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => true)
195
- - should allow to create a new profile via Person#profile_attributes
196
- - should allow to update an existing profile via Person#profile_attributes
197
- - should allow to delete an existing profile via Person#profile_attributes
198
-
199
- DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => :foo
200
- - should allow to create a new profile via Person#profile_attributes
201
- - should allow to update an existing profile via Person#profile_attributes
202
- - should not allow to delete an existing profile via Person#profile_attributes
203
-
204
- DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| true }
205
- - should not allow to create a new profile via Person#profile_attributes
206
- - should not allow to delete an existing profile via Person#profile_attributes
207
-
208
- DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| false }
209
- - should allow to create a new profile via Person#profile_attributes
210
- - should allow to update an existing profile via Person#profile_attributes
211
- - should not allow to delete an existing profile via Person#profile_attributes
212
-
213
- DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks)
214
- - should allow to create a new task via Project#tasks_attributes
215
- - should allow to update an existing task via Project#tasks_attributes
216
- - should not allow to delete an existing task via Profile#tasks_attributes
217
-
218
- DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => false)
219
- - should allow to create a new task via Project#tasks_attributes
220
- - should allow to update an existing task via Project#tasks_attributes
221
- - should not allow to delete an existing task via Profile#tasks_attributes
222
-
223
- DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => true)
224
- - should allow to create a new task via Project#tasks_attributes
225
- - should allow to update an existing task via Project#tasks_attributes
226
- - should allow to delete an existing task via Profile#tasks_attributes
227
-
228
- DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => :foo
229
- - should allow to create a new task via Project#tasks_attributes
230
- - should allow to update an existing task via Project#tasks_attributes
231
- - should not allow to delete an existing task via Profile#tasks_attributes
232
-
233
- DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| true }
234
- - should not allow to create a new task via Project#tasks_attributes
235
- - should not allow to delete an existing task via Profile#tasks_attributes
236
-
237
- DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| false }
238
- - should allow to create a new task via Project#tasks_attributes
239
- - should allow to update an existing task via Project#tasks_attributes
240
- - should not allow to delete an existing task via Profile#tasks_attributes
241
-
242
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects)
243
- - should allow to create a new project via Person#projects_attributes
244
- - should allow to update an existing project via Person#projects_attributes
245
- - should not allow to delete an existing project via Person#projects_attributes
85
+ h2. Current limitations
246
86
 
247
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy => false)
248
- - should allow to create a new project via Person#projects_attributes
249
- - should allow to update an existing project via Person#projects_attributes
250
- - should not allow to delete an existing project via Person#projects_attributes
87
+ There are some minor limitations at the moment but I hope that these will be resolved some time soon.
251
88
 
252
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy = true)
253
- - should allow to create a new project via Person#projects_attributes
254
- - should allow to update an existing project via Person#projects_attributes
255
- - should allow to delete an existing project via Person#projects_attributes
89
+ h3. marking for destruction
256
90
 
257
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => :foo
258
- - should allow to create a new project via Person#projects_attributes
259
- - should allow to update an existing project via Person#projects_attributes
260
- - should not allow to delete an existing project via Person#projects_attributes
91
+ @many_to_one, one_to_one and one_to_many@ relationships all perform the deleting of nested models by _marking them for destruction_. This means, that as long as you don't call #save on your resource, the nested models won't get destroyed. However, this is currently _not_ the case for @many_to_many@ relationships. The @join and target resources@ both get destroyed immediately when calling the nested attributes writer.
261
92
 
262
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| true }
263
- - should not allow to create a new project via Person#projects_attributes
264
- - should not allow to delete an existing project via Person#projects_attributes
93
+ For the above example this would mean that when you issue the code below, it would result in immediate deletion of the intermediate @ProjectMembership@ and the @Project@ resource.
265
94
 
266
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| false }
267
- - should allow to create a new project via Person#projects_attributes
268
- - should allow to update an existing project via Person#projects_attributes
269
- - should not allow to delete an existing project via Person#projects_attributes
95
+ <pre>
96
+ <code>
97
+ person.projects_attributes = { :id => valid_project_id, :_delete => true }
270
98
  </code>
271
99
  </pre>
100
+
101
+ h2. TODO
102
+
103
+ * collect validation errors from related resources
104
+ * update README to include more complete usecases
105
+ * think about replacing :reject_if with :if and :unless
data/TODO CHANGED
@@ -1,8 +1,6 @@
1
1
  TODO
2
2
  ====
3
3
 
4
- * add specs for :reject_if => :foo option, but think about _where_ first!
5
- * think about supporting :reject_unless in addition to :reject_if
6
- * think about generalizing :reject_if to not only work for new? resources
7
- * think about :allow_destroy accepting the same parameters like :reject_if
8
- (Symbol, String, #call)
4
+ * collect validation errors from related resources
5
+ * update README to include more complete usecases
6
+ * think about replacing :reject_if with :if and :unless
@@ -4,10 +4,8 @@ require 'dm-core'
4
4
  dir = Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes'
5
5
 
6
6
  require dir / 'model'
7
- #require dir / 'save'
8
7
  require dir / 'resource'
9
8
 
10
9
  # Activate the plugin
11
10
  DataMapper::Model.append_extensions(DataMapper::NestedAttributes::Model)
12
- #DataMapper::Model.append_inclusions(DataMapper::NestedAttributes::Save)
13
11
  DataMapper::Model.append_inclusions(DataMapper::NestedAttributes::CommonResourceSupport)
@@ -2,7 +2,8 @@ module DataMapper
2
2
  module NestedAttributes
3
3
 
4
4
  ##
5
- # raised by accepts_nested_attributes_for
5
+ # Named plugin exception that gets raised by
6
+ # @see accepts_nested_attributes_for
6
7
  # if the passed options don't make sense
7
8
  class InvalidOptions < ArgumentError; end
8
9
 
@@ -11,22 +12,26 @@ module DataMapper
11
12
  ##
12
13
  # Allows any association to accept nested attributes.
13
14
  #
14
- # @param association_name [Symbol, String]
15
+ # @param [Symbol, String] association_name
15
16
  # The name of the association that should accept nested attributes
16
17
  #
17
- # @param options [Hash, nil]
18
+ # @param [Hash, nil] options
18
19
  # List of resources to initialize the Collection with
19
20
  #
20
- # @option :reject_if [Symbol, String, #call]
21
+ # @option [Symbol, String, #call] :reject_if
21
22
  # An instance method name or an object that respond_to?(:call), which
22
23
  # stops a new record from being created, if it evaluates to true.
23
24
  #
24
- # @option :allow_destroy [true, false]
25
+ # @option [true, false] :allow_destroy
25
26
  # If true, allow destroying the association via the generated writer
26
27
  # If false, prevent destroying the association via the generated writer
27
28
  # defaults to false
28
29
  #
30
+ # @raise [DataMapper::NestedAttributes::InvalidOptions]
31
+ # A named exception class indicating invalid options
32
+ #
29
33
  # @return nil
34
+ #
30
35
  def accepts_nested_attributes_for(association_name, options = {})
31
36
 
32
37
  # ----------------------------------------------------------------------------------
@@ -51,8 +56,6 @@ module DataMapper
51
56
 
52
57
  include ::DataMapper::NestedAttributes::Resource
53
58
 
54
- add_save_behavior
55
-
56
59
  # TODO i wonder if this is the best place here?
57
60
  # the transactional save behavior is definitely not needed for all resources,
58
61
  # but it's necessary for resources that accept nested attributes
@@ -78,33 +81,58 @@ module DataMapper
78
81
  end
79
82
 
80
83
  ##
81
- # The options given to the accepts_nested_attributes method
82
- # They are guaranteed to be valid if they made it this far.
84
+ # The options given to the accepts_nested_attributes method.
85
+ #
86
+ # @return [Hash]
87
+ # The options given to the accepts_nested_attributes method
83
88
  #
84
- # @return [Hash] The options given to the accepts_nested_attributes method
85
- # @see accepts_nested_attributes
86
89
  def options_for_nested_attributes
87
90
  @options_for_nested_attributes ||= {}
88
91
  end
89
92
 
90
93
  private
91
94
 
92
- def add_save_behavior
93
- require Pathname(__FILE__).dirname.expand_path + 'save'
94
- include ::DataMapper::NestedAttributes::Save
95
- end
96
-
95
+ ##
96
+ # Provides a hook to include or disable customized transactional save behavior.
97
+ # Override this method to customize the implementation or disable it altogether.
98
+ # The current implementation in @see DataMapper::NestedAttributes::TransactionalSave
99
+ # simply wraps the saving of the complete object tree inside a transaction
100
+ # and rolls back in case any exceptions are raised, or any of the calls to
101
+ # @see DataMapper::Resource#save returned false
102
+ #
103
+ # @return Not specified
104
+ #
97
105
  def add_transactional_save_behavior
98
106
  require Pathname(__FILE__).dirname.expand_path + 'transactional_save'
99
107
  include ::DataMapper::NestedAttributes::TransactionalSave
100
108
  end
101
109
 
110
+ ##
111
+ # Provides a hook to include or disable customized error collecting behavior.
112
+ # Overwrite this method to customize the implementation or disable it altogether.
113
+ # The current implementation in @see DataMapper::NestedAttributes::ValidationErrorCollecting
114
+ # simply attaches all errors of related resources to the object that was initially saved.
115
+ #
116
+ # @return Not specified
117
+ #
102
118
  def add_error_collection_behavior
103
119
  require Pathname(__FILE__).dirname.expand_path + 'error_collecting'
104
120
  include ::DataMapper::NestedAttributes::ValidationErrorCollecting
105
121
  end
106
-
107
-
122
+
123
+ ##
124
+ # Checks options passed to @see accepts_nested_attributes_for
125
+ # If any of the given options is invalid, this method will raise
126
+ # @see DataMapper::NestedAttributes::InvalidOptions
127
+ #
128
+ # @param [Hash, nil] options
129
+ # The options passed to @see accepts_nested_attributes_for
130
+ #
131
+ # @raise [DataMapper::NestedAttributes::InvalidOptions]
132
+ # A named exception class indicating invalid options
133
+ #
134
+ # @return [nil]
135
+ #
108
136
  def assert_valid_options_for_nested_attributes(options)
109
137
 
110
138
  assert_kind_of 'options', options, Hash
@@ -1,6 +1,14 @@
1
1
  module DataMapper
2
2
  module NestedAttributes
3
3
 
4
+ ##
5
+ # Extensions and customizations for @see DataMapper::Resource
6
+ # that are needed if the @see 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.
4
12
  module Resource
5
13
 
6
14
  ##
@@ -15,16 +23,27 @@ module DataMapper
15
23
  # For now, this method basically is a no-op, but at least it provides a hook where
16
24
  # everyone can perform it's own sanitization by overwriting this method.
17
25
  #
18
- # @return [Hash] The sanitized attributes
26
+ # @param attributes [Hash]
27
+ # The attributes to sanitize
28
+ #
29
+ # @return [Hash]
30
+ # The sanitized attributes
31
+ #
19
32
  def sanitize_nested_attributes(attributes)
20
33
  attributes # noop
21
34
  end
22
35
 
23
36
  private
24
37
 
38
+ ##
25
39
  # Attribute hash keys that should not be assigned as normal attributes.
26
- # These hash keys are nested attributes implementation details.
27
- UNASSIGNABLE_KEYS = [ :id, :_delete ]
40
+ #
41
+ # @return [#each]
42
+ # The model key and :_delete, the latter being a special value
43
+ # used to mark a resource for destruction
44
+ def unassignable_keys
45
+ model.key.to_a << :_delete
46
+ end
28
47
 
29
48
 
30
49
  ##
@@ -50,12 +69,12 @@ module DataMapper
50
69
  def assign_nested_attributes_for_related_resource(relationship, attributes)
51
70
  if attributes[:id].blank?
52
71
  return if reject_new_record?(relationship, attributes)
53
- new_record = relationship.target_model.new(attributes.except(*UNASSIGNABLE_KEYS))
72
+ new_record = relationship.target_model.new(attributes.except(*unassignable_keys))
54
73
  relationship.set(self, new_record)
55
74
  else
56
75
  existing_record = relationship.get(self)
57
76
  if existing_record && existing_record.id.to_s == attributes[:id].to_s
58
- assign_to_or_mark_for_destruction(relationship, existing_record, attributes)
77
+ assign_or_mark_for_destruction(relationship, existing_record, attributes)
59
78
  end
60
79
  end
61
80
  end
@@ -104,11 +123,11 @@ module DataMapper
104
123
 
105
124
  if attributes[:id].blank?
106
125
  next if reject_new_record?(relationship, attributes)
107
- relationship.get(self).new(attributes.except(*UNASSIGNABLE_KEYS))
126
+ relationship.get(self).new(attributes.except(*unassignable_keys))
108
127
  else
109
128
  collection = relationship.get(self)
110
- if existing_record = collection.detect { |record| record.id.to_s == attributes[:id].to_s }
111
- assign_to_or_mark_for_destruction(relationship, existing_record, attributes)
129
+ if existing_record = collection.get(attributes[:id])
130
+ assign_or_mark_for_destruction(relationship, existing_record, attributes)
112
131
  end
113
132
  end
114
133
 
@@ -129,54 +148,82 @@ module DataMapper
129
148
  # All attributes except @see UNASSIGNABLE_KEYS will be assigned
130
149
  #
131
150
  # @return nil
132
- def assign_to_or_mark_for_destruction(relationship, resource, attributes)
151
+ def assign_or_mark_for_destruction(relationship, resource, attributes)
133
152
  allow_destroy = self.class.options_for_nested_attributes[relationship][:allow_destroy]
134
153
  if has_delete_flag?(attributes) && allow_destroy
135
- resource.mark_for_destruction
154
+ if relationship.is_a?(DataMapper::Associations::ManyToMany::Relationship)
155
+
156
+ target_query = relationship.target_key.zip(resource.key).to_hash
157
+ target_collection = relationship.get(self, target_query)
158
+
159
+ unless target_collection.empty?
160
+
161
+ target_collection.send(:intermediaries, target_collection).each do |intermediary|
162
+ intermediary.mark_for_destruction
163
+ end
164
+
165
+ target_collection.each { |r| r.mark_for_destruction }
166
+
167
+ end
168
+
169
+ else
170
+ resource.mark_for_destruction
171
+ end
136
172
  else
137
- resource.update(attributes.except(*UNASSIGNABLE_KEYS))
173
+ resource.attributes = attributes.except(*unassignable_keys)
174
+ resource.save
138
175
  end
139
176
  end
140
177
 
141
178
  ##
142
- # Determines if a hash contains a truthy _delete key.
179
+ # Determines if the given attributes hash contains a truthy :_delete key.
143
180
  #
144
- # @param hash [Hash] The hash to test
181
+ # @param attributes [Hash] The attributes to test
145
182
  #
146
- # @return [Boolean]
147
- # true, if hash containts a truthy _delete key
148
- # false, otherwise
149
- def has_delete_flag?(hash)
150
- # TODO find out if this activerecord code needs to be ported
151
- # ConnectionAdapters::Column.value_to_boolean hash['_delete']
152
- hash[:_delete]
183
+ # @return [TrueClass, FalseClass]
184
+ # true, if attributes contains a truthy :_delete key
185
+ def has_delete_flag?(attributes)
186
+ !!attributes[:_delete]
153
187
  end
154
188
 
155
189
  ##
156
- # Determines if a new record should be build by checking for
157
- # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
158
- # association and evaluates to +true+.
190
+ # Determines if a new record should be built with the given attributes.
191
+ # Rejects a new record if @see has_delete_flag? returns true for the given attributes,
192
+ # or if a :reject_if guard exists for the passed relationship that evaluates to +true+.
159
193
  #
160
194
  # @param relationship [DataMapper::Associations::Relationship]
161
- # The relationship backing the association.
162
- # Assignment will happen on the target end of the relationship
195
+ # The relationship backing the association.
163
196
  #
164
197
  # @param attributes [Hash]
165
- # The attributes to assign to the relationship's target end
166
- # All attributes except @see UNASSIGNABLE_KEYS will be assigned
198
+ # The attributes to test with @see has_delete_flag?
167
199
  #
168
- # @return [Boolean]
169
- # true, if the given attributes won't be rejected
170
- # false, otherwise
200
+ # @return [TrueClass, FalseClass]
201
+ # true, if the given attributes will be rejected
171
202
  def reject_new_record?(relationship, attributes)
172
203
  guard = self.class.options_for_nested_attributes[relationship][:reject_if]
173
204
  return false if guard.nil? # if relationship guard is nil, nothing will be rejected
174
205
  has_delete_flag?(attributes) || evaluate_reject_new_record_guard(guard, attributes)
175
206
  end
176
207
 
208
+ ##
209
+ # Evaluates the given guard by calling it with the given attributes
210
+ #
211
+ # @param [Symbol, String, #call] guard
212
+ # An instance method name or an object that respond_to?(:call), which
213
+ # would stop a new record from being created, if it evaluates to true.
214
+ #
215
+ # @param [Hash] attributes
216
+ # The attributes to pass to the guard for evaluating if it should reject
217
+ # the creation of a new resource
218
+ #
219
+ # @raise ArgumentError
220
+ # If the given guard doesn't match [Symbol, String, #call]
221
+ #
222
+ # @return [true, false]
223
+ # The value returned by evaluating the guard
177
224
  def evaluate_reject_new_record_guard(guard, attributes)
178
225
  if guard.is_a?(Symbol) || guard.is_a?(String)
179
- send(guard)
226
+ send(guard, attributes)
180
227
  elsif guard.respond_to?(:call)
181
228
  guard.call(attributes)
182
229
  else
@@ -185,18 +232,48 @@ module DataMapper
185
232
  end
186
233
  end
187
234
 
188
- def normalize_attributes_collection(attributes_collection)
189
- if attributes_collection.is_a?(Hash)
190
- attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
235
+ ##
236
+ # Make sure to return a collection of attribute hashes.
237
+ # If passed an attributes hash, map it to its attributes
238
+ #
239
+ # @param attributes [Hash, #each]
240
+ # An attributes hash or a collection of attribute hashes
241
+ #
242
+ # @return [#each]
243
+ # A collection of attribute hashes
244
+ def normalize_attributes_collection(attributes)
245
+ if attributes.is_a?(Hash)
246
+ attributes.map { |_, attributes| attributes }
191
247
  else
192
- attributes_collection
248
+ attributes
193
249
  end
194
250
  end
195
251
 
196
252
  end
197
253
 
254
+ ##
255
+ # This module provides basic support for accepting nested attributes,
256
+ # that every @see DataMapper::Resource must include. It includes methods
257
+ # that allow a resource to be marked for destruction and it provides an
258
+ # overwritten version of @see DataMapper::Resource#save_self that either
259
+ # destroys a resource if it's @see marked_for_destruction? or performs
260
+ # an ordinary save by delegating to super
261
+ #
198
262
  module CommonResourceSupport
199
263
 
264
+ ##
265
+ # If self is marked for destruction, destroy self
266
+ # else, save self by delegating to super method.
267
+ #
268
+ # @return The same value that super returns
269
+ def save_self
270
+ if marked_for_destruction?
271
+ saved? ? destroy : true
272
+ else
273
+ super
274
+ end
275
+ end
276
+
200
277
  ##
201
278
  # remove mark for destruction if present
202
279
  # before delegating reload behavior to super
@@ -210,11 +287,10 @@ module DataMapper
210
287
  ##
211
288
  # Test if this resource is marked for destruction
212
289
  #
213
- # @return [Boolean]
214
- # true, if this resource is marked for destruction
215
- # false, otherwise
290
+ # @return [true, false]
291
+ # true if this resource is marked for destruction
216
292
  def marked_for_destruction?
217
- @marked_for_destruction
293
+ !!@marked_for_destruction
218
294
  end
219
295
 
220
296
  ##