forked from datamapper/dm-core
/
one_to_many.rb
207 lines (174 loc) · 6.82 KB
/
one_to_many.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
require 'forwardable'
module DataMapper
module Associations
module OneToMany
OPTIONS = [ :class_name, :child_key, :parent_key, :min, :max, :remote_name ]
private
def one_to_many(name, options = {})
raise ArgumentError, "+name+ should be a Symbol (or Hash for +through+ support), but was #{name.class}", caller unless Symbol === name || Hash === name
raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options
relationship =
relationships(repository.name)[name] =
if options.include?(:through)
RelationshipChain.new(:child_model_name => options.fetch(:class_name, DataMapper::Inflection.classify(name)),
:parent_model_name => self.name,
:repository_name => repository.name,
:near_relationship_name => options[:through],
:remote_relationship_name => options.fetch(:remote_name, name),
:parent_key => options[:parent_key],
:child_key => options[:child_key])
else
relationships(repository.name)[name] =
Relationship.new(
DataMapper::Inflection.underscore(self.name.split('::').last).to_sym,
repository.name,
options.fetch(:class_name, DataMapper::Inflection.classify(name)),
self.name,
options
)
end
class_eval <<-EOS, __FILE__, __LINE__
def #{name}(options={})
options.empty? ? #{name}_association : #{name}_association.all(options)
end
def #{name}=(children)
#{name}_association.replace(children)
end
private
def #{name}_association
@#{name}_association ||= begin
relationship = self.class.relationships(#{repository.name.inspect})[#{name.inspect}]
raise ArgumentError.new("Relationship #{name.inspect} does not exist") unless relationship
association = Proxy.new(relationship, self)
parent_associations << association
association
end
end
EOS
relationship
end
class Proxy
instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? should should_not ].include?(m) }
def replace(resources)
each { |resource| remove_resource(resource) }
append_resource(resources)
children.replace(resources)
self
end
def push(*resources)
append_resource(resources)
children.push(*resources)
self
end
def unshift(*resources)
append_resource(resources)
children.unshift(*resources)
self
end
def <<(resource)
#
# The order here is of the essence.
#
# self.append_resource used to be called before children.<<, which created weird errors
# where the resource was appended in the db before it was appended onto the @children
# structure, that was just read from the database, and therefore suddenly had two
# elements instead of one after the first addition.
#
children << resource
append_resource([ resource ])
self
end
def pop
remove_resource(children.pop)
end
def shift
remove_resource(children.shift)
end
def delete(resource, &block)
remove_resource(children.delete(resource, &block))
end
def delete_at(index)
remove_resource(children.delete_at(index))
end
def clear
each { |resource| remove_resource(resource) }
children.clear
self
end
def save
save_resources(@dirty_children)
@dirty_children = []
self
end
def all(options={})
options.empty? ? children : @relationship.get_children(@parent_resource,options,:all)
end
def first(options={})
options.empty? ? children.first : @relationship.get_children(@parent_resource,options,:first)
end
def reload!
@dirty_children = []
@children = nil
self
end
private
def initialize(relationship, parent_resource)
# raise ArgumentError, "+relationship+ should be a DataMapper::Association::Relationship, but was #{relationship.class}", caller unless Relationship === relationship
# raise ArgumentError, "+parent_resource+ should be a DataMapper::Resource, but was #{parent_resource.class}", caller unless Resource === parent_resource
@relationship = relationship
@parent_resource = parent_resource
@dirty_children = []
end
def children
@children ||= @relationship.get_children(@parent_resource)
end
def ensure_mutable
raise ImmutableAssociationError, "You can not modify this assocation" if RelationshipChain === @relationship
end
def add_default_association_values(resources)
resources.each do |resource|
conditions = @relationship.query.reject { |key, value| key == :order }
conditions.each do |key, value|
resource.send("#{key}=", value) if key.class != DataMapper::Query::Operator && resource.send("#{key}") == nil
end
end
resources
end
def remove_resource(resource)
ensure_mutable
begin
repository(@relationship.repository_name) do
@relationship.attach_parent(resource, nil)
resource.save
end
rescue
children << resource
raise
end
resource
end
def append_resource(resources = [])
ensure_mutable
add_default_association_values(resources)
if @parent_resource.new_record?
@dirty_children.push(*resources)
else
save_resources(resources)
end
end
def save_resources(resources = [])
ensure_mutable
repository(@relationship.repository_name) do
resources.each do |resource|
@relationship.attach_parent(resource, @parent_resource)
resource.save
end
end
end
def method_missing(method, *args, &block)
children.__send__(method, *args, &block)
end
end # class Proxy
end # module OneToMany
end # module Associations
end # module DataMapper