-
Notifications
You must be signed in to change notification settings - Fork 63
/
reflection.rb
344 lines (287 loc) · 10.9 KB
/
reflection.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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
module ActiveFedora
module Reflection # :nodoc:
extend ActiveSupport::Concern
included do
class_attribute :reflections
self.reflections = {}
end
module ClassMethods
def create_reflection(macro, name, options, active_fedora)
klass = case macro
when :has_many, :belongs_to, :has_and_belongs_to_many, :contains
AssociationReflection
when :rdf, :singular_rdf
RDFPropertyReflection
end
reflection = klass.new(macro, name, options, active_fedora)
add_reflection name, reflection
reflection
end
def add_reflection(name, reflection)
# FIXME this is where the problem with association_spec is caused (key is string now)
self.reflections = self.reflections.merge(name => reflection)
end
# Returns a hash containing all AssociationReflection objects for the current class.
# Example:
#
# Invoice.reflections
# Account.reflections
#
def reflections
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
end
def outgoing_reflections
reflections.select { |_, reflection| reflection.kind_of? RDFPropertyReflection }
end
def child_resource_reflections
reflections.select { |_, reflection| reflection.kind_of?(AssociationReflection) && reflection.macro == :contains }
end
# Returns the AssociationReflection object for the +association+ (use the symbol).
#
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
val = reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
unless val
# When a has_many is paired with a has_and_belongs_to_many the assocation will have a plural name
association = association.to_s.pluralize.to_sym
val = reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
val
end
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
end
class MacroReflection
# Returns the name of the macro.
#
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
# Returns the macro type.
#
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
attr_reader :macro
# Returns the hash of options used for the macro.
#
# <tt>has_many :clients</tt> returns +{}+
attr_reader :options
attr_reader :active_fedora
# Returns the target association's class.
#
# class Author < ActiveRecord::Base
# has_many :books
# end
#
# Author.reflect_on_association(:books).klass
# # => Book
#
# <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
@klass ||= class_name.constantize
end
def initialize(macro, name, options, active_fedora)
@macro, @name, @options, @active_fedora = macro, name, options, active_fedora
@automatic_inverse_of = nil
end
# Returns a new, unsaved instance of the associated class. +options+ will
# be passed to the class's constructor.
def build_association(*options)
klass.new(*options)
end
# Returns the class name for the macro.
#
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
def class_name
@class_name ||= options[:class_name] || derive_class_name
end
# Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
@collection
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
def belongs_to?
macro == :belongs_to
end
def has_many?
macro == :has_many
end
def has_and_belongs_to_many?
macro == :has_and_belongs_to_many
end
private
def derive_class_name
class_name = name.to_s.camelize
class_name = class_name.singularize if collection?
class_name
end
def derive_foreign_key
if belongs_to?
"#{name}_id"
elsif has_and_belongs_to_many?
"#{name.to_s.singularize}_ids"
elsif options[:as]
"#{options[:as]}_id"
else
# This works well if this is a has_many that is the inverse of a belongs_to, but it isn't correct for a has_many that is the invers of a has_and_belongs_to_many
active_fedora.name.foreign_key
end
end
end
# Holds all the meta-data about an association as it was specified in the
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def initialize(macro, name, options, active_fedora)
super
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
end
# Returns a new, unsaved instance of the associated class. +options+ will
# be passed to the class's constructor.
def build_association(*options)
klass.new(*options)
end
# Creates a new instance of the associated class, and immediately saves it
# with ActiveFedora::Base#save. +options+ will be passed to the class's
# creation method. Returns the newly created object.
def create_association(*options)
klass.create(*options)
end
def foreign_key
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
# Returns the RDF predicate as defined by the :predicate option
def predicate
options[:predicate]
end
def solr_key
@solr_key ||= begin
predicate_string = predicate.fragment || predicate.to_s.rpartition(/\//).last
ActiveFedora::SolrQueryBuilder.solr_name(predicate_string, :symbol)
end
end
def check_validity!
check_validity_of_inverse!
end
def check_validity_of_inverse!
unless options[:polymorphic]
if has_inverse? && inverse_of.nil?
raise InverseOfAssociationNotFoundError.new(self)
end
end
end
# A chain of reflections from this one back to the owner. For more see the explanation in
# ThroughReflection.
def chain
[self]
end
alias :source_macro :macro
def has_inverse?
inverse_name
end
def inverse_of
return unless inverse_name
@inverse_of ||= klass.reflect_on_association inverse_name
end
# Returns whether or not the association should be validated as part of
# the parent's validation.
#
# Unless you explicitly disable validation with
# <tt>:validate => false</tt>, validation will take place when:
#
# * you explicitly enable validation; <tt>:validate => true</tt>
# * you use autosave; <tt>:autosave => true</tt>
# * the association is a +has_many+ association
def validate?
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
end
def association_class
case macro
when :contains
Associations::ContainsAssociation
when :belongs_to
Associations::BelongsToAssociation
when :has_and_belongs_to_many
Associations::HasAndBelongsToManyAssociation
when :has_many
Associations::HasManyAssociation
when :singular_rdf
Associations::SingularRDF
when :rdf
Associations::RDF
end
end
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_and_belongs_to_many, :belongs_to]
INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
private
def inverse_name
options.fetch(:inverse_of) do
if @automatic_inverse_of == false
nil
else
@automatic_inverse_of ||= automatic_inverse_of
end
end
end
# Checks if the inverse reflection that is returned from the
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
#
# Note: klass will always be valid because when there's a NameError
# from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
klass.name == reflection.active_fedora.name &&
can_find_inverse_of_automatically?(reflection)
end
# returns either false or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
inverse_name = ActiveSupport::Inflector.underscore(active_fedora.name).to_sym
begin
reflection = klass.reflect_on_association(inverse_name)
rescue NameError
# Give up: we couldn't compute the klass type so we won't be able
# to find any associations either.
reflection = false
end
if valid_inverse_reflection?(reflection)
return inverse_name
end
end
false
end
# Checks to see if the reflection doesn't have any options that prevent
# us from being able to guess the inverse automatically. First, the
# <tt>inverse_of</tt> option cannot be set to false. Second, we must
# have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
# Third, we must not have options such as <tt>:polymorphic</tt> or
# <tt>:foreign_key</tt> which prevent us from correctly guessing the
# inverse association.
#
# Anything with a scope can additionally ruin our attempt at finding an
# inverse, so we exclude reflections with scopes.
def can_find_inverse_of_automatically?(reflection)
reflection.options[:inverse_of] != false &&
VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
!INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] }
#&& !reflection.scope
end
end
class RDFPropertyReflection < AssociationReflection
def derive_foreign_key
name
end
def derive_class_name
class_name = name.to_s.sub(/_ids?$/, '').camelize
class_name = class_name.singularize if collection?
class_name
end
end
end
end