snusnu-dm-accepts_nested_attributes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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