snusnu-dm-accepts_nested_attributes 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ test_log
2
+ pkg
3
+ pkg/*
4
+ */pkg/*
5
+ bundle
6
+ bundle/*
7
+ doc
8
+ *.log
9
+ log
10
+ !log*.rb
11
+ */log
12
+ log/*
13
+ */log/*
14
+ coverage
15
+ */coverage
16
+ lib/dm-more.rb
17
+ *.db
18
+ nbproject
19
+ .DS_Store
20
+ rspec_report.html
21
+ *.swp
22
+ _Yardoc
23
+ */ri
data/History.txt ADDED
@@ -0,0 +1 @@
1
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Martin Gamsjäger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,29 @@
1
+ .gitignore
2
+ History.txt
3
+ LICENSE
4
+ Manifest.txt
5
+ README.textile
6
+ Rakefile
7
+ TODO
8
+ lib/dm-accepts_nested_attributes.rb
9
+ lib/dm-accepts_nested_attributes/associations.rb
10
+ lib/dm-accepts_nested_attributes/nested_attributes.rb
11
+ lib/dm-accepts_nested_attributes/version.rb
12
+ spec/fixtures/person.rb
13
+ spec/fixtures/profile.rb
14
+ spec/fixtures/project.rb
15
+ spec/fixtures/project_membership.rb
16
+ spec/fixtures/task.rb
17
+ spec/integration/belongs_to_spec.rb
18
+ spec/integration/has_1_spec.rb
19
+ spec/integration/has_n_spec.rb
20
+ spec/integration/has_n_through_spec.rb
21
+ spec/shared/rspec_tmbundle_support.rb
22
+ spec/spec.opts
23
+ spec/spec_helper.rb
24
+ spec/unit/accepts_nested_attributes_for_spec.rb
25
+ spec/unit/resource_spec.rb
26
+ tasks/gemspec.rb
27
+ tasks/hoe.rb
28
+ tasks/install.rb
29
+ tasks/spec.rb
data/README.textile ADDED
@@ -0,0 +1,250 @@
1
+ h2. dm-accepts_nested_attributes
2
+
3
+ A DataMapper plugin that allows nested model attribute assignment like activerecord does.
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
+ h3. Why isn't this implemented as options on association declarations?
12
+
13
+ * I somehow like the declarative style of @accepts_nested_attributes_for@ better. it jumps out immediately.
14
+ * The API for datamapper and activerecord is the same.
15
+ * association definitions can already get quite long and "unreadable". chances are you overlook it!
16
+
17
+ h3. Why doesn't accepts_nested_attributes_for take more than one association_name?
18
+
19
+ While writing the unit specs for this method, I realised that there are way too many ways to call this
20
+ method, which makes it "hard" to spec all possible calls. That's why I started to list Pros and Cons, and
21
+ decided to support only one @association_name@ per call, at least for now.
22
+
23
+ h4. Pros
24
+
25
+ * less complex code
26
+ * fewer ways to call the method (simpler to understand, easier to spec)
27
+ * easier to read (nr of calls == nr of accessible associations, this could be seen as a con also)
28
+ * easier (and more extensible) option handling
29
+ ** options don't implicitly apply to _all_ associations (could be seen as a con also?)
30
+ ** options can explicitly be applied to _only the desired_ associations
31
+ ** reject_if option maybe often makes more sense on exactly _one_ associaton (maybe not?)
32
+ * no question what happens if the association_name is invalid (the whole call is invalid)
33
+ ** with at least one _invalid_ association_name, what happens for the other _valid_ ones?
34
+
35
+ h4. Cons
36
+
37
+ * needs more method calls (overhead should be minimal)
38
+ * options that apply to more than one attribute need to be duplicated (maybe a Pro because of readability)
39
+
40
+ h3. Examples
41
+
42
+ The following example illustrates the use of this plugin.
43
+
44
+ <pre>
45
+ <code>
46
+ require "rubygems"
47
+
48
+ gem 'dm-core', '0.9.11'
49
+ gem 'dm-validations', '0.9.11'
50
+ gem 'dm-accepts_nested_attributes', '0.0.1'
51
+
52
+ require "dm-core"
53
+ require "dm-validations"
54
+ require "dm-accepts_nested_attributes"
55
+
56
+ DataMapper::Logger.new(STDOUT, :debug)
57
+ DataMapper.setup(:default, 'sqlite3::memory:')
58
+
59
+ class Person
60
+ include DataMapper::Resource
61
+ property :id, Serial
62
+ property :name, String
63
+ has 1, :profile
64
+ has n, :project_memberships
65
+ has n, :projects, :through => :project_memberships
66
+ accepts_nested_attributes_for :profile
67
+ accepts_nested_attributes_for :projects
68
+
69
+ # adds the following instance methods
70
+ # #profile_attributes
71
+ # #projects_attributes
72
+ end
73
+
74
+ class Profile
75
+ include DataMapper::Resource
76
+ property :id, Serial
77
+ property :person_id, Integer
78
+ belongs_to :person
79
+ accepts_nested_attributes_for :person
80
+
81
+ # adds the following instance methods
82
+ # #person_attributes
83
+ end
84
+
85
+ class Project
86
+ include DataMapper::Resource
87
+ property :id, Serial
88
+ has n, :tasks
89
+ has n, :project_memberships
90
+ has n, :people, :through => :project_memberships
91
+ accepts_nested_attributes_for :tasks
92
+ accepts_nested_attributes_for :people
93
+
94
+ # adds the following instance methods
95
+ # #tasks_attributes
96
+ # #people_attributes
97
+ end
98
+
99
+ class ProjectMembership
100
+ include DataMapper::Resource
101
+ property :id, Serial
102
+ property :person_id, Integer
103
+ property :project_id, Integer
104
+ belongs_to :person
105
+ belongs_to :project
106
+
107
+ # nothing added here
108
+ # code only listed to provide complete example env
109
+ end
110
+
111
+ class Task
112
+ include DataMapper::Resource
113
+ property :id, Serial
114
+ property :project_id, Integer
115
+ belongs_to :project
116
+
117
+ # nothing added here
118
+ # code only listed to provide complete example env
119
+ end
120
+
121
+ DataMapper.auto_migrate!
122
+ </code>
123
+ </pre>
124
+
125
+ h2. Current Integration Specs
126
+
127
+ <pre>
128
+ <code>
129
+ DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person)
130
+ - should allow to create a new person via Profile#person_attributes
131
+ - should allow to update an existing person via Profile#person_attributes
132
+ - should not allow to delete an existing person via Profile#person_attributes
133
+
134
+ DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy => false)
135
+ - should allow to create a new person via Profile#person_attributes
136
+ - should allow to update an existing person via Profile#person_attributes
137
+ - should not allow to delete an existing person via Profile#person_attributes
138
+
139
+ DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy = true)
140
+ - should allow to create a new person via Profile#person_attributes
141
+ - should allow to update an existing person via Profile#person_attributes
142
+ - should allow to delete an existing person via Profile#person_attributes
143
+
144
+ DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => :foo
145
+ - should allow to create a new person via Profile#person_attributes
146
+ - should allow to update an existing person via Profile#person_attributes
147
+ - should not allow to delete an existing person via Profile#person_attributes
148
+
149
+ DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| true }
150
+ - should not allow to create a new person via Profile#person_attributes
151
+ - should not allow to delete an existing person via Profile#person_attributes
152
+
153
+ DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| false }
154
+ - should allow to create a new person via Profile#person_attributes
155
+ - should allow to update an existing person via Profile#person_attributes
156
+ - should not allow to delete an existing person via Profile#person_attributes
157
+
158
+ DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile)
159
+ - should allow to create a new profile via Person#profile_attributes
160
+ - should allow to update an existing profile via Person#profile_attributes
161
+ - should not allow to delete an existing profile via Person#profile_attributes
162
+
163
+ DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => false)
164
+ - should allow to create a new profile via Person#profile_attributes
165
+ - should allow to update an existing profile via Person#profile_attributes
166
+ - should not allow to delete an existing profile via Person#profile_attributes
167
+
168
+ DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => true)
169
+ - should allow to create a new profile via Person#profile_attributes
170
+ - should allow to update an existing profile via Person#profile_attributes
171
+ - should allow to delete an existing profile via Person#profile_attributes
172
+
173
+ DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => :foo
174
+ - should allow to create a new profile via Person#profile_attributes
175
+ - should allow to update an existing profile via Person#profile_attributes
176
+ - should not allow to delete an existing profile via Person#profile_attributes
177
+
178
+ DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| true }
179
+ - should not allow to create a new profile via Person#profile_attributes
180
+ - should not allow to delete an existing profile via Person#profile_attributes
181
+
182
+ DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| false }
183
+ - should allow to create a new profile via Person#profile_attributes
184
+ - should allow to update an existing profile via Person#profile_attributes
185
+ - should not allow to delete an existing profile via Person#profile_attributes
186
+
187
+ DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks)
188
+ - should allow to create a new task via Project#tasks_attributes
189
+ - should allow to update an existing task via Project#tasks_attributes
190
+ - should not allow to delete an existing task via Profile#tasks_attributes
191
+
192
+ DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => false)
193
+ - should allow to create a new task via Project#tasks_attributes
194
+ - should allow to update an existing task via Project#tasks_attributes
195
+ - should not allow to delete an existing task via Profile#tasks_attributes
196
+
197
+ DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => true)
198
+ - should allow to create a new task via Project#tasks_attributes
199
+ - should allow to update an existing task via Project#tasks_attributes
200
+ - should allow to delete an existing task via Profile#tasks_attributes
201
+
202
+ DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => :foo
203
+ - should allow to create a new task via Project#tasks_attributes
204
+ - should allow to update an existing task via Project#tasks_attributes
205
+ - should not allow to delete an existing task via Profile#tasks_attributes
206
+
207
+ DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| true }
208
+ - should not allow to create a new task via Project#tasks_attributes
209
+ - should not allow to delete an existing task via Profile#tasks_attributes
210
+
211
+ DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| false }
212
+ - should allow to create a new task via Project#tasks_attributes
213
+ - should allow to update an existing task via Project#tasks_attributes
214
+ - should not allow to delete an existing task via Profile#tasks_attributes
215
+
216
+ DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects)
217
+ - should allow to create a new project via Person#projects_attributes
218
+ - should allow to update an existing project via Person#projects_attributes
219
+ - should not allow to delete an existing project via Person#projects_attributes
220
+
221
+ DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy => false)
222
+ - should allow to create a new project via Person#projects_attributes
223
+ - should allow to update an existing project via Person#projects_attributes
224
+ - should not allow to delete an existing project via Person#projects_attributes
225
+
226
+ DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy = true)
227
+ - should allow to create a new project via Person#projects_attributes
228
+ - should allow to update an existing project via Person#projects_attributes
229
+ - should allow to delete an existing project via Person#projects_attributes
230
+
231
+ DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => :foo
232
+ - should allow to create a new project via Person#projects_attributes
233
+ - should allow to update an existing project via Person#projects_attributes
234
+ - should not allow to delete an existing project via Person#projects_attributes
235
+
236
+ DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| true }
237
+ - should not allow to create a new project via Person#projects_attributes
238
+ - should not allow to delete an existing project via Person#projects_attributes
239
+
240
+ DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| false }
241
+ - should allow to create a new project via Person#projects_attributes
242
+ - should allow to update an existing project via Person#projects_attributes
243
+ - should not allow to delete an existing project via Person#projects_attributes
244
+ </code>
245
+ </pre>
246
+
247
+ h2. TODO
248
+
249
+ * add more specs and fix bugs
250
+ * Adapt to datamapper/next
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'pathname'
2
+ require 'rubygems'
3
+ require 'rake'
4
+ require 'rake/rdoctask'
5
+
6
+ ROOT = Pathname(__FILE__).dirname.expand_path
7
+ JRUBY = RUBY_PLATFORM =~ /java/
8
+ WINDOWS = Gem.win_platform?
9
+ SUDO = (WINDOWS || JRUBY) ? '' : ('sudo' unless ENV['SUDOLESS'])
10
+
11
+ require ROOT + 'lib/dm-accepts_nested_attributes/version'
12
+
13
+ AUTHOR = "Martin Gamsjäger"
14
+ EMAIL = "gamsnjaga [a] gmail [d] com"
15
+ GEM_NAME = "dm-accepts_nested_attributes"
16
+ GEM_VERSION = DataMapper::NestedAttributes::VERSION
17
+
18
+ GEM_DEPENDENCIES = [
19
+ ["dm-core", '>=0.9.11'],
20
+ ['addressable', '~>2.0.2' ]
21
+ ]
22
+
23
+ GEM_CLEAN = %w[ log pkg coverage ]
24
+ GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.textile LICENSE TODO History.txt ] }
25
+
26
+ PROJECT_NAME = "dm-accepts_nested_attributes"
27
+ PROJECT_URL = "http://github.com/snusnu/dm-accepts_nested_attributes/tree/master"
28
+ PROJECT_DESCRIPTION = PROJECT_SUMMARY = %{
29
+ A DataMapper plugin that adds the possibility to perform nested model attribute assignment
30
+ }
31
+
32
+ Pathname.glob(ROOT.join('tasks/**/*.rb').to_s).each { |f| require f }
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ TODO
2
+ ====
3
+
@@ -0,0 +1,18 @@
1
+ # Needed to import datamapper and other gems
2
+ require 'rubygems'
3
+ require 'pathname'
4
+
5
+ # Add all external dependencies for the plugin here
6
+ gem 'dm-core', '>=0.9.11'
7
+ gem 'dm-validations', '>=0.9.11'
8
+
9
+ require 'dm-core'
10
+ require 'dm-validations'
11
+
12
+ # Require plugin-files
13
+ require Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes' / 'nested_attributes'
14
+ # monkeypatches for dm-core/associations/(many_to_one.rb and one_to_many.rb)
15
+ require Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes' / 'associations'
16
+
17
+ # Include the plugin in Model
18
+ DataMapper::Resource.append_inclusions DataMapper::NestedAttributes
@@ -0,0 +1,55 @@
1
+ module DataMapper
2
+ module Associations
3
+
4
+ module ManyToOne
5
+
6
+ class Proxy
7
+
8
+ def save
9
+
10
+ return false if @parent.nil?
11
+
12
+ # original dm-core-0.9.11 code:
13
+ # return true unless parent.new_record?
14
+
15
+ # and the backwards compatible extension to it (allows update of belongs_to model)
16
+ if !parent.new_record? && !@relationship.child_model.autosave_associations.key?(@relationship.name)
17
+ return true
18
+ end
19
+
20
+ @relationship.with_repository(parent) do
21
+ result = parent.marked_for_destruction? ? parent.destroy : parent.save
22
+ @relationship.child_key.set(@child, @relationship.parent_key.get(parent)) if result
23
+ result
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+
33
+ module OneToMany
34
+
35
+ class Proxy
36
+
37
+ private
38
+
39
+ def save_resource(resource, parent = @parent)
40
+ @relationship.with_repository(resource) do |r|
41
+ if parent.nil? && resource.model.respond_to?(:many_to_many)
42
+ resource.destroy
43
+ else
44
+ @relationship.attach_parent(resource, parent)
45
+ resource.marked_for_destruction? ? resource.destroy : resource.save
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,300 @@
1
+ module DataMapper
2
+ module NestedAttributes
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.class_inheritable_accessor :autosave_associations
7
+ base.autosave_associations = {}
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Defines an attributes writer for the specified association(s). If you
13
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
14
+ # will need to add the attribute writer to the allowed list.
15
+ #
16
+ # Supported options:
17
+ # [:allow_destroy]
18
+ # If true, destroys any members from the attributes hash with a
19
+ # <tt>_delete</tt> key and a value that evaluates to +true+
20
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
21
+ # [:reject_if]
22
+ # Allows you to specify a Proc that checks whether a record should be
23
+ # built for a certain attribute hash. The hash is passed to the Proc
24
+ # and the Proc should return either +true+ or +false+. When no Proc
25
+ # is specified a record will be built for all attribute hashes that
26
+ # do not have a <tt>_delete</tt> that evaluates to true.
27
+ #
28
+ # Examples:
29
+ # # creates avatar_attributes=
30
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
31
+ # # creates avatar_attributes= and posts_attributes=
32
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
33
+ def accepts_nested_attributes_for(association_name, options = {})
34
+
35
+ assert_kind_of 'association_name', association_name, Symbol, String
36
+ assert_kind_of 'options', options, Hash
37
+
38
+ options = { :allow_destroy => false }.update(options)
39
+
40
+ # raises if the specified option keys aren't valid
41
+ assert_valid_autosave_options(options)
42
+
43
+ # raises if the specified association doesn't exist
44
+ # we don't need the return value here, just the check
45
+ # ------------------------------------------------------
46
+ # also, when using the return value from this call to
47
+ # replace association_name with association.name,
48
+ # has(1, :through) are broken, because they seem to have
49
+ # a different name
50
+
51
+ association_for_name(association_name)
52
+
53
+ autosave_associations[association_name] = options
54
+
55
+ type = nr_of_possible_child_instances(association_name) > 1 ? :collection : :one_to_one
56
+
57
+ class_eval %{
58
+
59
+ def #{association_name}_attributes=(attributes)
60
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
61
+ end
62
+
63
+ if association_type(:#{association_name}) == :many_to_one || association_type(:#{association_name}) == :one_to_one
64
+
65
+ def get_#{association_name}
66
+ #{association_name.to_s} || self.class.associated_model_for_name(:#{association_name}).new
67
+ end
68
+
69
+ end
70
+
71
+ }, __FILE__, __LINE__ + 1
72
+
73
+ end
74
+
75
+ def reject_new_nested_attributes_proc_for(association_name)
76
+ autosave_associations[association_name] ? autosave_associations[association_name][:reject_if] : nil
77
+ end
78
+
79
+
80
+ # utility methods
81
+
82
+ def nr_of_possible_child_instances(association_name, repository = :default)
83
+ # belongs_to seems to generate no options[:max]
84
+ association_for_name(association_name, repository).options[:max] || 1
85
+ end
86
+
87
+ # i have the feeling this should be refactored
88
+ def associated_model_for_name(association_name, repository = :default)
89
+ a = association_for_name(association_name, repository)
90
+ case association_type(association_name)
91
+ when :many_to_one
92
+ a.parent_model
93
+ when :one_to_one
94
+ a.child_model
95
+ when :one_to_many
96
+ a.child_model
97
+ when :many_to_many
98
+ Object.full_const_get(a.options[:child_model])
99
+ else
100
+ raise ArgumentError, "Unknown association type #{a.inspect}"
101
+ end
102
+ end
103
+
104
+ # maybe this should be provided by dm-core somehow
105
+ # DataMapper::Association::Relationship would be a place maybe?
106
+ def association_type(association_name)
107
+ a = association_for_name(association_name)
108
+ if a.options[:max].nil? # belongs_to
109
+ :many_to_one
110
+ elsif a.options[:max] == 1 # has(1)
111
+ :one_to_one
112
+ elsif a.options[:max] > 1 && !a.is_a?(DataMapper::Associations::RelationshipChain) # has(n)
113
+ :one_to_many
114
+ elsif a.is_a?(DataMapper::Associations::RelationshipChain) # has(n, :through) MUST be checked after has(n) here
115
+ :many_to_many
116
+ else
117
+ raise ArgumentError, "Unknown association type #{a.inspect}"
118
+ end
119
+ end
120
+
121
+ # avoid nil access by always going through this
122
+ # this method raises if the association named name is not established in this model
123
+ def association_for_name(name, repository = :default)
124
+ association = self.relationships(repository)[name]
125
+ # TODO think about using a specific Error class like UnknownAssociationError
126
+ raise(ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}") unless association
127
+ association
128
+ end
129
+
130
+ private
131
+
132
+ # think about storing valid options in a classlevel constant
133
+ def assert_valid_autosave_options(options)
134
+ unless options.all? { |k,v| [ :allow_destroy, :reject_if ].include?(k) }
135
+ raise ArgumentError, 'accepts_nested_attributes_for only takes :allow_destroy and :reject_if as options'
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+
142
+ # instance methods
143
+
144
+ # returns nil if no resource has been associated yet
145
+ def associated_instance_get(association_name, repository = :default)
146
+ send(self.class.association_for_name(association_name, repository).name)
147
+ end
148
+
149
+ # Reloads the attributes of the object as usual and removes a mark for destruction.
150
+ def reload
151
+ @marked_for_destruction = false
152
+ super
153
+ end
154
+
155
+ def marked_for_destruction?
156
+ @marked_for_destruction
157
+ end
158
+
159
+ def mark_for_destruction
160
+ @marked_for_destruction = true
161
+ end
162
+
163
+
164
+ private
165
+
166
+ # Attribute hash keys that should not be assigned as normal attributes.
167
+ # These hash keys are nested attributes implementation details.
168
+ UNASSIGNABLE_KEYS = [ :id, :_delete ]
169
+
170
+
171
+ # Assigns the given attributes to the association.
172
+ #
173
+ # If the given attributes include an <tt>:id</tt> that matches the existing
174
+ # record’s id, then the existing record will be modified. Otherwise a new
175
+ # record will be built.
176
+ #
177
+ # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
178
+ # <tt>:_delete</tt> key set to a truthy value, then the existing record
179
+ # will be marked for destruction.
180
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
181
+ if attributes[:id].blank?
182
+ unless reject_new_record?(association_name, attributes)
183
+ model = self.class.associated_model_for_name(association_name)
184
+ send("#{association_name}=", model.new(attributes.except(*UNASSIGNABLE_KEYS)))
185
+ end
186
+ else (existing_record = associated_instance_get(association_name)) && existing_record.id.to_s == attributes[:id].to_s
187
+ assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
188
+ end
189
+ end
190
+
191
+ # Assigns the given attributes to the collection association.
192
+ #
193
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
194
+ # will update that record. Hashes without an <tt>:id</tt> value will build
195
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
196
+ # value and a <tt>:_delete</tt> key set to a truthy value will mark the
197
+ # matched record for destruction.
198
+ #
199
+ # For example:
200
+ #
201
+ # assign_nested_attributes_for_collection_association(:people, {
202
+ # '1' => { :id => '1', :name => 'Peter' },
203
+ # '2' => { :name => 'John' },
204
+ # '3' => { :id => '2', :_delete => true }
205
+ # })
206
+ #
207
+ # Will update the name of the Person with ID 1, build a new associated
208
+ # person with the name `John', and mark the associatied Person with ID 2
209
+ # for destruction.
210
+ #
211
+ # Also accepts an Array of attribute hashes:
212
+ #
213
+ # assign_nested_attributes_for_collection_association(:people, [
214
+ # { :id => '1', :name => 'Peter' },
215
+ # { :name => 'John' },
216
+ # { :id => '2', :_delete => true }
217
+ # ])
218
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
219
+
220
+ assert_kind_of 'association_name', association_name, Symbol
221
+ assert_kind_of 'attributes_collection', attributes_collection, Hash, Array
222
+
223
+ if attributes_collection.is_a? Hash
224
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
225
+ end
226
+
227
+ attributes_collection.each do |attributes|
228
+ if attributes[:id].blank?
229
+ unless reject_new_record?(association_name, attributes)
230
+ case self.class.association_type(association_name)
231
+ when :one_to_many
232
+ build_new_has_n_association(association_name, attributes)
233
+ when :many_to_many
234
+ build_new_has_n_through_association(association_name, attributes)
235
+ end
236
+ end
237
+ elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes[:id].to_s }
238
+ assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+ def build_new_has_n_association(association_name, attributes)
245
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
246
+ end
247
+
248
+ def build_new_has_n_through_association(association_name, attributes)
249
+ # fetch the association to have the information ready
250
+ association = self.class.association_for_name(association_name)
251
+
252
+ # do what's done in dm-core/specs/integration/association_through_spec.rb
253
+
254
+ # explicitly build the join entry and assign it to the join association
255
+ join_entry = Extlib::Inflection.constantize(Extlib::Inflection.classify(association.name)).new
256
+ self.send(association.name) << join_entry
257
+ self.save
258
+ # explicitly build the child entry and assign the join entry to its join association
259
+ child_entry = self.class.associated_model_for_name(association_name).new(attributes)
260
+ child_entry.send(association.name) << join_entry
261
+ child_entry.save
262
+ end
263
+
264
+ # Updates a record with the +attributes+ or marks it for destruction if
265
+ # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
266
+ def assign_to_or_mark_for_destruction(association_name, record, attributes, allow_destroy)
267
+ if has_delete_flag?(attributes) && allow_destroy
268
+ if self.class.association_type(association_name) == :many_to_many
269
+ # destroy the join record
270
+ record.send(self.class.association_for_name(association_name).name).destroy!
271
+ # destroy the child record
272
+ record.destroy
273
+ else
274
+ record.mark_for_destruction
275
+ end
276
+ else
277
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
278
+ if self.class.association_type(association_name) == :many_to_many
279
+ record.save
280
+ end
281
+ end
282
+ end
283
+
284
+ # Determines if a hash contains a truthy _delete key.
285
+ def has_delete_flag?(hash)
286
+ # TODO find out if this activerecord code needs to be ported
287
+ # ConnectionAdapters::Column.value_to_boolean hash['_delete']
288
+ hash[:_delete]
289
+ end
290
+
291
+ # Determines if a new record should be build by checking for
292
+ # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
293
+ # association and evaluates to +true+.
294
+ def reject_new_record?(association_name, attributes)
295
+ guard = self.class.reject_new_nested_attributes_proc_for(association_name)
296
+ has_delete_flag?(attributes) || (guard.respond_to?(:call) && guard.call(attributes))
297
+ end
298
+
299
+ end
300
+ end