formed 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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