formed 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +146 -0
  3. data/Rakefile +12 -0
  4. data/lib/active_form.rb +12 -0
  5. data/lib/formed/acts_like_model.rb +27 -0
  6. data/lib/formed/association_relation.rb +22 -0
  7. data/lib/formed/associations/association.rb +193 -0
  8. data/lib/formed/associations/builder/association.rb +116 -0
  9. data/lib/formed/associations/builder/collection_association.rb +71 -0
  10. data/lib/formed/associations/builder/has_many.rb +24 -0
  11. data/lib/formed/associations/builder/has_one.rb +44 -0
  12. data/lib/formed/associations/builder/singular_association.rb +46 -0
  13. data/lib/formed/associations/builder.rb +13 -0
  14. data/lib/formed/associations/collection_association.rb +296 -0
  15. data/lib/formed/associations/collection_proxy.rb +519 -0
  16. data/lib/formed/associations/foreign_association.rb +37 -0
  17. data/lib/formed/associations/has_many_association.rb +63 -0
  18. data/lib/formed/associations/has_one_association.rb +27 -0
  19. data/lib/formed/associations/singular_association.rb +66 -0
  20. data/lib/formed/associations.rb +62 -0
  21. data/lib/formed/attributes.rb +42 -0
  22. data/lib/formed/base.rb +183 -0
  23. data/lib/formed/core.rb +73 -0
  24. data/lib/formed/from_model.rb +41 -0
  25. data/lib/formed/from_params.rb +33 -0
  26. data/lib/formed/inheritance.rb +179 -0
  27. data/lib/formed/nested_attributes.rb +287 -0
  28. data/lib/formed/reflection.rb +781 -0
  29. data/lib/formed/relation/delegation.rb +147 -0
  30. data/lib/formed/relation.rb +113 -0
  31. data/lib/formed/version.rb +3 -0
  32. data/lib/generators/active_form/form_generator.rb +72 -0
  33. data/lib/generators/active_form/templates/form.rb.tt +8 -0
  34. data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
  35. data/lib/generators/active_form/templates/module.rb.tt +4 -0
  36. 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