formed 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +146 -0
- data/Rakefile +12 -0
- data/lib/active_form.rb +12 -0
- data/lib/formed/acts_like_model.rb +27 -0
- data/lib/formed/association_relation.rb +22 -0
- data/lib/formed/associations/association.rb +193 -0
- data/lib/formed/associations/builder/association.rb +116 -0
- data/lib/formed/associations/builder/collection_association.rb +71 -0
- data/lib/formed/associations/builder/has_many.rb +24 -0
- data/lib/formed/associations/builder/has_one.rb +44 -0
- data/lib/formed/associations/builder/singular_association.rb +46 -0
- data/lib/formed/associations/builder.rb +13 -0
- data/lib/formed/associations/collection_association.rb +296 -0
- data/lib/formed/associations/collection_proxy.rb +519 -0
- data/lib/formed/associations/foreign_association.rb +37 -0
- data/lib/formed/associations/has_many_association.rb +63 -0
- data/lib/formed/associations/has_one_association.rb +27 -0
- data/lib/formed/associations/singular_association.rb +66 -0
- data/lib/formed/associations.rb +62 -0
- data/lib/formed/attributes.rb +42 -0
- data/lib/formed/base.rb +183 -0
- data/lib/formed/core.rb +73 -0
- data/lib/formed/from_model.rb +41 -0
- data/lib/formed/from_params.rb +33 -0
- data/lib/formed/inheritance.rb +179 -0
- data/lib/formed/nested_attributes.rb +287 -0
- data/lib/formed/reflection.rb +781 -0
- data/lib/formed/relation/delegation.rb +147 -0
- data/lib/formed/relation.rb +113 -0
- data/lib/formed/version.rb +3 -0
- data/lib/generators/active_form/form_generator.rb +72 -0
- data/lib/generators/active_form/templates/form.rb.tt +8 -0
- data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
- data/lib/generators/active_form/templates/module.rb.tt +4 -0
- metadata +203 -0
@@ -0,0 +1,781 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Reflection # :nodoc:
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_reflections, instance_writer: false, default: {}
|
9
|
+
class_attribute :aggregate_reflections, instance_writer: false, default: {}
|
10
|
+
class_attribute :automatic_scope_inversing, instance_writer: false, default: false
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def create(macro, name, scope, options, ar)
|
15
|
+
reflection = reflection_class_for(macro).new(name, scope, options, ar)
|
16
|
+
options[:through] ? ThroughReflection.new(reflection) : reflection
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_reflection(ar, name, reflection)
|
20
|
+
ar.clear_reflections_cache
|
21
|
+
name = -name.to_s
|
22
|
+
ar._reflections = ar._reflections.except(name).merge!(name => reflection)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_aggregate_reflection(ar, name, reflection)
|
26
|
+
ar.aggregate_reflections = ar.aggregate_reflections.merge(-name.to_s => reflection)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def reflection_class_for(macro)
|
32
|
+
case macro
|
33
|
+
when :composed_of
|
34
|
+
AggregateReflection
|
35
|
+
when :has_many
|
36
|
+
HasManyReflection
|
37
|
+
when :has_one
|
38
|
+
HasOneReflection
|
39
|
+
when :belongs_to
|
40
|
+
BelongsToReflection
|
41
|
+
else
|
42
|
+
raise "Unsupported Macro: #{macro}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# \Reflection enables the ability to examine the associations and aggregations of
|
48
|
+
# Active Record classes and objects. This information, for example,
|
49
|
+
# can be used in a form builder that takes an Active Record object
|
50
|
+
# and creates input fields for all of the attributes depending on their type
|
51
|
+
# and displays the associations to other objects.
|
52
|
+
#
|
53
|
+
# MacroReflection class has info for AggregateReflection and AssociationReflection
|
54
|
+
# classes.
|
55
|
+
module ClassMethods
|
56
|
+
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
57
|
+
def reflect_on_all_aggregations
|
58
|
+
aggregate_reflections.values
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
|
62
|
+
#
|
63
|
+
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
|
64
|
+
#
|
65
|
+
def reflect_on_aggregation(aggregation)
|
66
|
+
aggregate_reflections[aggregation.to_s]
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
|
70
|
+
#
|
71
|
+
# Account.reflections # => {"balance" => AggregateReflection}
|
72
|
+
#
|
73
|
+
def reflections
|
74
|
+
@reflections ||= begin
|
75
|
+
ref = {}
|
76
|
+
|
77
|
+
_reflections.each do |name, reflection|
|
78
|
+
parent_reflection = reflection.parent_reflection
|
79
|
+
|
80
|
+
if parent_reflection
|
81
|
+
parent_name = parent_reflection.name
|
82
|
+
ref[parent_name.to_s] = parent_reflection
|
83
|
+
else
|
84
|
+
ref[name] = reflection
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
ref
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns an array of AssociationReflection objects for all the
|
93
|
+
# associations in the class. If you only want to reflect on a certain
|
94
|
+
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
|
95
|
+
# <tt>:belongs_to</tt>) as the first parameter.
|
96
|
+
#
|
97
|
+
# Example:
|
98
|
+
#
|
99
|
+
# Account.reflect_on_all_associations # returns an array of all associations
|
100
|
+
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
|
101
|
+
#
|
102
|
+
def reflect_on_all_associations(macro = nil)
|
103
|
+
association_reflections = reflections.values
|
104
|
+
association_reflections.select! { |reflection| reflection.macro == macro } if macro
|
105
|
+
association_reflections
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the AssociationReflection object for the +association+ (use the symbol).
|
109
|
+
#
|
110
|
+
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
|
111
|
+
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
|
112
|
+
#
|
113
|
+
def reflect_on_association(association)
|
114
|
+
reflections[association.to_s]
|
115
|
+
end
|
116
|
+
|
117
|
+
def _reflect_on_association(association) # :nodoc:
|
118
|
+
_reflections[association.to_s]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
122
|
+
def reflect_on_all_autosave_associations
|
123
|
+
reflections.values.select { |reflection| reflection.options[:autosave] }
|
124
|
+
end
|
125
|
+
|
126
|
+
def clear_reflections_cache # :nodoc:
|
127
|
+
@__reflections = nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Holds all the methods that are shared between MacroReflection and ThroughReflection.
|
132
|
+
#
|
133
|
+
# AbstractReflection
|
134
|
+
# MacroReflection
|
135
|
+
# AggregateReflection
|
136
|
+
# AssociationReflection
|
137
|
+
# HasManyReflection
|
138
|
+
# HasOneReflection
|
139
|
+
# BelongsToReflection
|
140
|
+
# HasAndBelongsToManyReflection
|
141
|
+
# ThroughReflection
|
142
|
+
# PolymorphicReflection
|
143
|
+
# RuntimeReflection
|
144
|
+
class AbstractReflection # :nodoc:
|
145
|
+
def through_reflection?
|
146
|
+
false
|
147
|
+
end
|
148
|
+
|
149
|
+
def table_name
|
150
|
+
klass.table_name
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns a new, unsaved instance of the associated class. +attributes+ will
|
154
|
+
# be passed to the class's constructor.
|
155
|
+
def build_association(attributes, &block)
|
156
|
+
klass.new(attributes, &block)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the class name for the macro.
|
160
|
+
#
|
161
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
|
162
|
+
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
|
163
|
+
def class_name
|
164
|
+
@class_name ||= -(options[:class_name] || derive_class_name).to_s
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns a list of scopes that should be applied for this Reflection
|
168
|
+
# object when querying the database.
|
169
|
+
def scopes
|
170
|
+
[]
|
171
|
+
end
|
172
|
+
|
173
|
+
def constraints
|
174
|
+
chain.flat_map(&:scopes)
|
175
|
+
end
|
176
|
+
|
177
|
+
def inverse_of
|
178
|
+
return unless inverse_name
|
179
|
+
|
180
|
+
@inverse_of ||= klass._reflect_on_association inverse_name
|
181
|
+
end
|
182
|
+
|
183
|
+
def check_validity_of_inverse!
|
184
|
+
return if polymorphic?
|
185
|
+
raise InverseOfAssociationNotFoundError, self if has_inverse? && inverse_of.nil?
|
186
|
+
raise InverseOfAssociationRecursiveError, self if has_inverse? && inverse_of == self
|
187
|
+
end
|
188
|
+
|
189
|
+
def alias_candidate(name)
|
190
|
+
"#{plural_name}_#{name}"
|
191
|
+
end
|
192
|
+
|
193
|
+
def chain
|
194
|
+
collect_join_chain
|
195
|
+
end
|
196
|
+
|
197
|
+
def build_scope(table, predicate_builder = predicate_builder(table), klass = self.klass)
|
198
|
+
Relation.create(
|
199
|
+
klass,
|
200
|
+
table: table,
|
201
|
+
predicate_builder: predicate_builder
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
205
|
+
def strict_loading?
|
206
|
+
options[:strict_loading]
|
207
|
+
end
|
208
|
+
|
209
|
+
def strict_loading_violation_message(owner)
|
210
|
+
message = +"`#{owner}` is marked for strict_loading."
|
211
|
+
message << " The #{polymorphic? ? "polymorphic association" : "#{klass} association"}"
|
212
|
+
message << " named `:#{name}` cannot be lazily loaded."
|
213
|
+
end
|
214
|
+
|
215
|
+
protected
|
216
|
+
|
217
|
+
# FIXME: this is a horrible name
|
218
|
+
def actual_source_reflection
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def predicate_builder(table)
|
225
|
+
PredicateBuilder.new(TableMetadata.new(klass, table))
|
226
|
+
end
|
227
|
+
|
228
|
+
def primary_key(klass)
|
229
|
+
klass.primary_key || raise(UnknownPrimaryKey, klass)
|
230
|
+
end
|
231
|
+
|
232
|
+
def ensure_option_not_given_as_class!(option_name)
|
233
|
+
return unless options[option_name].instance_of?(Class)
|
234
|
+
|
235
|
+
raise ArgumentError, "A class was passed to `:#{option_name}` but we are expecting a string."
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Base class for AggregateReflection and AssociationReflection. Objects of
|
240
|
+
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
241
|
+
class MacroReflection < AbstractReflection
|
242
|
+
# Returns the name of the macro.
|
243
|
+
#
|
244
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
|
245
|
+
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
|
246
|
+
attr_reader :name
|
247
|
+
|
248
|
+
attr_reader :scope, :active_form, :plural_name
|
249
|
+
|
250
|
+
# Returns the hash of options used for the macro.
|
251
|
+
#
|
252
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
|
253
|
+
# <tt>has_many :clients</tt> returns <tt>{}</tt>
|
254
|
+
attr_reader :options # :nodoc:
|
255
|
+
|
256
|
+
def initialize(name, scope, options, active_form)
|
257
|
+
@name = name
|
258
|
+
@scope = scope
|
259
|
+
@options = options
|
260
|
+
@active_form = active_form
|
261
|
+
@klass = options[:anonymous_class]
|
262
|
+
end
|
263
|
+
|
264
|
+
def autosave=(autosave)
|
265
|
+
@options[:autosave] = autosave
|
266
|
+
parent_reflection = self.parent_reflection
|
267
|
+
parent_reflection.autosave = autosave if parent_reflection
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns the class for the macro.
|
271
|
+
#
|
272
|
+
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
|
273
|
+
# <tt>has_many :clients</tt> returns the Client class
|
274
|
+
#
|
275
|
+
# class Company < ActiveRecord::Base
|
276
|
+
# has_many :clients
|
277
|
+
# end
|
278
|
+
#
|
279
|
+
# Company.reflect_on_association(:clients).klass
|
280
|
+
# # => Client
|
281
|
+
#
|
282
|
+
# <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
|
283
|
+
# a new association object. Use +build_association+ or +create_association+
|
284
|
+
# instead. This allows plugins to hook into association object creation.
|
285
|
+
def klass
|
286
|
+
@klass ||= compute_class(class_name)
|
287
|
+
end
|
288
|
+
|
289
|
+
def compute_class(name)
|
290
|
+
name.constantize
|
291
|
+
end
|
292
|
+
|
293
|
+
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_form+ attribute,
|
294
|
+
# and +other_aggregation+ has an options hash assigned to it.
|
295
|
+
def ==(other)
|
296
|
+
super ||
|
297
|
+
other.is_a?(self.class) &&
|
298
|
+
name == other.name &&
|
299
|
+
!other.options.nil? &&
|
300
|
+
active_form == other.active_form
|
301
|
+
end
|
302
|
+
|
303
|
+
def scope_for(relation, owner = nil)
|
304
|
+
relation.instance_exec(owner, &scope) || relation
|
305
|
+
end
|
306
|
+
|
307
|
+
private
|
308
|
+
|
309
|
+
def derive_class_name
|
310
|
+
"#{name.to_s.camelize}Form"
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Holds all the metadata about an aggregation as it was specified in the
|
315
|
+
# Active Record class.
|
316
|
+
class AggregateReflection < MacroReflection # :nodoc:
|
317
|
+
def mapping
|
318
|
+
mapping = options[:mapping] || [name, name]
|
319
|
+
mapping.first.is_a?(Array) ? mapping : [mapping]
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Holds all the metadata about an association as it was specified in the
|
324
|
+
# Active Record class.
|
325
|
+
class AssociationReflection < MacroReflection # :nodoc:
|
326
|
+
def compute_class(name)
|
327
|
+
raise ArgumentError, "Polymorphic associations do not support computing the class." if polymorphic?
|
328
|
+
|
329
|
+
msg = <<-MSG.squish
|
330
|
+
Formed couldn't find a valid form for #{name} association.
|
331
|
+
Please provide the :class_name option on the association declaration.
|
332
|
+
If :class_name is already provided, make sure it's an Formed::Base subclass.
|
333
|
+
MSG
|
334
|
+
|
335
|
+
begin
|
336
|
+
klass = active_form.send(:compute_type, name)
|
337
|
+
|
338
|
+
raise ArgumentError, msg unless klass < Formed::Base
|
339
|
+
|
340
|
+
klass
|
341
|
+
rescue NameError
|
342
|
+
raise NameError, msg
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
attr_reader :type, :foreign_type
|
347
|
+
attr_accessor :parent_reflection # Reflection
|
348
|
+
|
349
|
+
def initialize(name, scope, options, active_form)
|
350
|
+
super
|
351
|
+
@type = -(options[:foreign_type].to_s || "#{options[:as]}_type") if options[:as]
|
352
|
+
@foreign_type = -(options[:foreign_type].to_s || "#{name}_type") if options[:polymorphic]
|
353
|
+
|
354
|
+
ensure_option_not_given_as_class!(:class_name)
|
355
|
+
end
|
356
|
+
|
357
|
+
def join_table
|
358
|
+
@join_table ||= -(options[:join_table].to_s || derive_join_table)
|
359
|
+
end
|
360
|
+
|
361
|
+
def foreign_key
|
362
|
+
@foreign_key ||= -(options[:foreign_key].to_s || derive_foreign_key)
|
363
|
+
end
|
364
|
+
|
365
|
+
def association_foreign_key
|
366
|
+
@association_foreign_key ||= -(options[:association_foreign_key].to_s || class_name.foreign_key)
|
367
|
+
end
|
368
|
+
|
369
|
+
def association_primary_key(klass = nil)
|
370
|
+
primary_key(klass || self.klass)
|
371
|
+
end
|
372
|
+
|
373
|
+
def check_validity!
|
374
|
+
check_validity_of_inverse!
|
375
|
+
end
|
376
|
+
|
377
|
+
def check_eager_loadable!
|
378
|
+
return unless scope
|
379
|
+
|
380
|
+
return if scope.arity.zero?
|
381
|
+
|
382
|
+
raise ArgumentError, <<-MSG.squish
|
383
|
+
The association scope '#{name}' is instance dependent (the scope
|
384
|
+
block takes an argument). Eager loading instance dependent scopes
|
385
|
+
is not supported.
|
386
|
+
MSG
|
387
|
+
end
|
388
|
+
|
389
|
+
def through_reflection
|
390
|
+
nil
|
391
|
+
end
|
392
|
+
|
393
|
+
def source_reflection
|
394
|
+
self
|
395
|
+
end
|
396
|
+
|
397
|
+
# A chain of reflections from this one back to the owner. For more see the explanation in
|
398
|
+
# ThroughReflection.
|
399
|
+
def collect_join_chain
|
400
|
+
[self]
|
401
|
+
end
|
402
|
+
|
403
|
+
def nested?
|
404
|
+
false
|
405
|
+
end
|
406
|
+
|
407
|
+
def has_scope?
|
408
|
+
scope
|
409
|
+
end
|
410
|
+
|
411
|
+
def has_inverse?
|
412
|
+
inverse_name
|
413
|
+
end
|
414
|
+
|
415
|
+
def polymorphic_inverse_of(associated_class)
|
416
|
+
return unless has_inverse?
|
417
|
+
unless (inverse_relationship = associated_class._reflect_on_association(options[:inverse_of]))
|
418
|
+
raise InverseOfAssociationNotFoundError.new(self, associated_class)
|
419
|
+
end
|
420
|
+
|
421
|
+
inverse_relationship
|
422
|
+
end
|
423
|
+
|
424
|
+
# Returns the macro type.
|
425
|
+
#
|
426
|
+
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
|
427
|
+
def macro
|
428
|
+
raise NotImplementedError
|
429
|
+
end
|
430
|
+
|
431
|
+
# Returns whether or not this association reflection is for a collection
|
432
|
+
# association. Returns +true+ if the +macro+ is either +has_many+ or
|
433
|
+
# +has_and_belongs_to_many+, +false+ otherwise.
|
434
|
+
def collection?
|
435
|
+
false
|
436
|
+
end
|
437
|
+
|
438
|
+
# Returns whether or not the association should be validated as part of
|
439
|
+
# the parent's validation.
|
440
|
+
#
|
441
|
+
# Unless you explicitly disable validation with
|
442
|
+
# <tt>validate: false</tt>, validation will take place when:
|
443
|
+
#
|
444
|
+
# * you explicitly enable validation; <tt>validate: true</tt>
|
445
|
+
# * you use autosave; <tt>autosave: true</tt>
|
446
|
+
# * the association is a +has_many+ association
|
447
|
+
def validate?
|
448
|
+
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
|
449
|
+
end
|
450
|
+
|
451
|
+
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
452
|
+
def belongs_to?
|
453
|
+
false
|
454
|
+
end
|
455
|
+
|
456
|
+
# Returns +true+ if +self+ is a +has_one+ reflection.
|
457
|
+
def has_one?
|
458
|
+
false
|
459
|
+
end
|
460
|
+
|
461
|
+
def association_class
|
462
|
+
raise NotImplementedError
|
463
|
+
end
|
464
|
+
|
465
|
+
def polymorphic?
|
466
|
+
options[:polymorphic]
|
467
|
+
end
|
468
|
+
|
469
|
+
def add_as_source(seed)
|
470
|
+
seed
|
471
|
+
end
|
472
|
+
|
473
|
+
def add_as_polymorphic_through(reflection, seed)
|
474
|
+
seed + [PolymorphicReflection.new(self, reflection)]
|
475
|
+
end
|
476
|
+
|
477
|
+
def add_as_through(seed)
|
478
|
+
seed + [self]
|
479
|
+
end
|
480
|
+
|
481
|
+
def extensions
|
482
|
+
Array(options[:extend])
|
483
|
+
end
|
484
|
+
|
485
|
+
private
|
486
|
+
|
487
|
+
# Attempts to find the inverse association name automatically.
|
488
|
+
# If it cannot find a suitable inverse association name, it returns
|
489
|
+
# +nil+.
|
490
|
+
def inverse_name
|
491
|
+
@inverse_name = options.fetch(:inverse_of) { automatic_inverse_of } unless defined?(@inverse_name)
|
492
|
+
|
493
|
+
@inverse_name
|
494
|
+
end
|
495
|
+
|
496
|
+
# returns either +nil+ or the inverse association name that it finds.
|
497
|
+
def automatic_inverse_of
|
498
|
+
return unless can_find_inverse_of_automatically?(self)
|
499
|
+
|
500
|
+
inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_form.name.demodulize).to_sym
|
501
|
+
|
502
|
+
begin
|
503
|
+
reflection = klass._reflect_on_association(inverse_name)
|
504
|
+
rescue NameError
|
505
|
+
# Give up: we couldn't compute the klass type so we won't be able
|
506
|
+
# to find any associations either.
|
507
|
+
reflection = false
|
508
|
+
end
|
509
|
+
|
510
|
+
inverse_name if valid_inverse_reflection?(reflection)
|
511
|
+
end
|
512
|
+
|
513
|
+
# Checks if the inverse reflection that is returned from the
|
514
|
+
# +automatic_inverse_of+ method is a valid reflection. We must
|
515
|
+
# make sure that the reflection's active_record name matches up
|
516
|
+
# with the current reflection's klass name.
|
517
|
+
def valid_inverse_reflection?(reflection)
|
518
|
+
reflection &&
|
519
|
+
reflection != self &&
|
520
|
+
foreign_key == reflection.foreign_key &&
|
521
|
+
klass <= reflection.active_record &&
|
522
|
+
can_find_inverse_of_automatically?(reflection, true)
|
523
|
+
end
|
524
|
+
|
525
|
+
# Checks to see if the reflection doesn't have any options that prevent
|
526
|
+
# us from being able to guess the inverse automatically. First, the
|
527
|
+
# <tt>inverse_of</tt> option cannot be set to false. Second, we must
|
528
|
+
# have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
|
529
|
+
# Third, we must not have options such as <tt>:foreign_key</tt>
|
530
|
+
# which prevent us from correctly guessing the inverse association.
|
531
|
+
def can_find_inverse_of_automatically?(reflection, inverse_reflection = false)
|
532
|
+
reflection.options[:inverse_of] != false &&
|
533
|
+
!reflection.options[:through] &&
|
534
|
+
scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
|
535
|
+
end
|
536
|
+
|
537
|
+
# Scopes on the potential inverse reflection prevent automatic
|
538
|
+
# <tt>inverse_of</tt>, since the scope could exclude the owner record
|
539
|
+
# we would inverse from. Scopes on the reflection itself allow for
|
540
|
+
# automatic <tt>inverse_of</tt> as long as
|
541
|
+
# <tt>config.active_record.automatic_scope_inversing<tt> is set to
|
542
|
+
# +true+ (the default for new applications).
|
543
|
+
def scope_allows_automatic_inverse_of?(reflection, inverse_reflection)
|
544
|
+
if inverse_reflection
|
545
|
+
!reflection.scope
|
546
|
+
else
|
547
|
+
!reflection.scope || reflection.klass.automatic_scope_inversing
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
def derive_class_name
|
552
|
+
class_name = name.to_s
|
553
|
+
class_name = class_name.singularize if collection?
|
554
|
+
class_name.camelize
|
555
|
+
end
|
556
|
+
|
557
|
+
def derive_foreign_key
|
558
|
+
if belongs_to?
|
559
|
+
"#{name}_id"
|
560
|
+
elsif options[:as]
|
561
|
+
"#{options[:as]}_id"
|
562
|
+
else
|
563
|
+
active_form.model_name.to_s.foreign_key
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
def derive_join_table
|
568
|
+
ModelSchema.derive_join_table_name active_form.table_name, klass.table_name
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
class HasManyReflection < AssociationReflection # :nodoc:
|
573
|
+
def macro
|
574
|
+
:has_many
|
575
|
+
end
|
576
|
+
|
577
|
+
def collection?
|
578
|
+
true
|
579
|
+
end
|
580
|
+
|
581
|
+
def association_class
|
582
|
+
if options[:through]
|
583
|
+
Associations::HasManyThroughAssociation
|
584
|
+
else
|
585
|
+
Associations::HasManyAssociation
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
class HasOneReflection < AssociationReflection # :nodoc:
|
591
|
+
def macro
|
592
|
+
:has_one
|
593
|
+
end
|
594
|
+
|
595
|
+
def has_one?
|
596
|
+
true
|
597
|
+
end
|
598
|
+
|
599
|
+
def association_class
|
600
|
+
if options[:through]
|
601
|
+
Associations::HasOneThroughAssociation
|
602
|
+
else
|
603
|
+
Associations::HasOneAssociation
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
class BelongsToReflection < AssociationReflection # :nodoc:
|
609
|
+
def macro
|
610
|
+
:belongs_to
|
611
|
+
end
|
612
|
+
|
613
|
+
def belongs_to?
|
614
|
+
true
|
615
|
+
end
|
616
|
+
|
617
|
+
def association_class
|
618
|
+
if polymorphic?
|
619
|
+
Associations::BelongsToPolymorphicAssociation
|
620
|
+
else
|
621
|
+
Associations::BelongsToAssociation
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
# klass option is necessary to support loading polymorphic associations
|
626
|
+
def association_primary_key(klass = nil)
|
627
|
+
if (primary_key = options[:primary_key])
|
628
|
+
@association_primary_key ||= -primary_key.to_s
|
629
|
+
else
|
630
|
+
primary_key(klass || self.klass)
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
private
|
635
|
+
|
636
|
+
def can_find_inverse_of_automatically?(*)
|
637
|
+
!polymorphic? && super
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
|
642
|
+
def macro
|
643
|
+
:has_and_belongs_to_many
|
644
|
+
end
|
645
|
+
|
646
|
+
def collection?
|
647
|
+
true
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
# Holds all the metadata about a :through association as it was specified
|
652
|
+
# in the Active Record class.
|
653
|
+
class ThroughReflection < AbstractReflection # :nodoc:
|
654
|
+
delegate :foreign_key, :foreign_type, :association_foreign_key, :join_id_for, :type,
|
655
|
+
:active_record_primary_key, :join_foreign_key, to: :source_reflection
|
656
|
+
|
657
|
+
def initialize(delegate_reflection)
|
658
|
+
@delegate_reflection = delegate_reflection
|
659
|
+
@klass = delegate_reflection.options[:anonymous_class]
|
660
|
+
@source_reflection_name = delegate_reflection.options[:source]
|
661
|
+
|
662
|
+
ensure_option_not_given_as_class!(:source_type)
|
663
|
+
end
|
664
|
+
|
665
|
+
def through_reflection?
|
666
|
+
true
|
667
|
+
end
|
668
|
+
|
669
|
+
def klass
|
670
|
+
@klass ||= delegate_reflection.compute_class(class_name)
|
671
|
+
end
|
672
|
+
|
673
|
+
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
|
674
|
+
#
|
675
|
+
# class Post < ActiveRecord::Base
|
676
|
+
# has_many :taggings
|
677
|
+
# has_many :tags, through: :taggings
|
678
|
+
# end
|
679
|
+
#
|
680
|
+
# tags_reflection = Post.reflect_on_association(:tags)
|
681
|
+
# tags_reflection.source_reflection_names
|
682
|
+
# # => [:tag, :tags]
|
683
|
+
#
|
684
|
+
def source_reflection_names
|
685
|
+
options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq
|
686
|
+
end
|
687
|
+
|
688
|
+
def check_validity!
|
689
|
+
raise HasManyThroughAssociationNotFoundError.new(active_record, self) if through_reflection.nil?
|
690
|
+
|
691
|
+
if through_reflection.polymorphic?
|
692
|
+
raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self) if has_one?
|
693
|
+
|
694
|
+
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
|
695
|
+
|
696
|
+
end
|
697
|
+
|
698
|
+
raise HasManyThroughSourceAssociationNotFoundError, self if source_reflection.nil?
|
699
|
+
|
700
|
+
if options[:source_type] && !source_reflection.polymorphic?
|
701
|
+
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
702
|
+
end
|
703
|
+
|
704
|
+
if source_reflection.polymorphic? && options[:source_type].nil?
|
705
|
+
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
|
706
|
+
end
|
707
|
+
|
708
|
+
if has_one? && through_reflection.collection?
|
709
|
+
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
|
710
|
+
end
|
711
|
+
|
712
|
+
if parent_reflection.nil?
|
713
|
+
reflections = active_record.reflections.keys.map(&:to_sym)
|
714
|
+
|
715
|
+
if reflections.index(through_reflection.name) > reflections.index(name)
|
716
|
+
raise HasManyThroughOrderError.new(active_record.name, self, through_reflection)
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
check_validity_of_inverse!
|
721
|
+
end
|
722
|
+
|
723
|
+
private
|
724
|
+
|
725
|
+
attr_reader :delegate_reflection
|
726
|
+
|
727
|
+
def inverse_name
|
728
|
+
delegate_reflection.send(:inverse_name)
|
729
|
+
end
|
730
|
+
|
731
|
+
def derive_class_name
|
732
|
+
# get the class_name of the belongs_to association of the through reflection
|
733
|
+
options[:source_type] || source_reflection.class_name
|
734
|
+
end
|
735
|
+
|
736
|
+
delegate_methods = AssociationReflection.public_instance_methods -
|
737
|
+
public_instance_methods
|
738
|
+
|
739
|
+
delegate(*delegate_methods, to: :delegate_reflection)
|
740
|
+
end
|
741
|
+
|
742
|
+
class PolymorphicReflection < AbstractReflection # :nodoc:
|
743
|
+
delegate :klass, :scope, :plural_name, :type, :join_primary_key, :join_foreign_key,
|
744
|
+
:name, :scope_for, to: :@reflection
|
745
|
+
|
746
|
+
def initialize(reflection, previous_reflection)
|
747
|
+
@reflection = reflection
|
748
|
+
@previous_reflection = previous_reflection
|
749
|
+
end
|
750
|
+
|
751
|
+
def constraints
|
752
|
+
@reflection.constraints + [source_type_scope]
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
class RuntimeReflection < AbstractReflection # :nodoc:
|
757
|
+
delegate :scope, :type, :constraints, :join_foreign_key, to: :@reflection
|
758
|
+
|
759
|
+
def initialize(reflection, association)
|
760
|
+
@reflection = reflection
|
761
|
+
@association = association
|
762
|
+
end
|
763
|
+
|
764
|
+
def klass
|
765
|
+
@association.klass
|
766
|
+
end
|
767
|
+
|
768
|
+
def aliased_table
|
769
|
+
klass.arel_table
|
770
|
+
end
|
771
|
+
|
772
|
+
def join_primary_key(klass = self.klass)
|
773
|
+
@reflection.join_primary_key(klass)
|
774
|
+
end
|
775
|
+
|
776
|
+
def all_includes
|
777
|
+
yield
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|
781
|
+
end
|