aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/reflection.rb
blob: de5a01c9be9f471501a41bb3933c3491eead6fa5 (plain) (blame)
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
module ActiveRecord
  module Reflection # :nodoc:
    def self.append_features(base)
      super
      base.extend(ClassMethods)

      base.class_eval do
        class << self
          alias_method :composed_of_without_reflection, :composed_of

          def composed_of_with_reflection(part_id, options = {})
            composed_of_without_reflection(part_id, options)
            reflect_on_all_aggregations << AggregateReflection.new(:composed_of, part_id, options, self)
          end

          alias_method :composed_of, :composed_of_with_reflection
        end
      end

      for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
        base.module_eval <<-"end_eval"
          class << self
            alias_method :#{association_type}_without_reflection, :#{association_type}

            def #{association_type}_with_reflection(association_id, options = {})
              #{association_type}_without_reflection(association_id, options)
              reflect_on_all_associations << AssociationReflection.new(:#{association_type}, association_id, options, self)
            end

            alias_method :#{association_type}, :#{association_type}_with_reflection
          end
        end_eval
      end
    end

    # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
    # This information can, for example, be used in a form builder that took an Active Record object and created input
    # fields for all of the attributes depending on their type and displayed the associations to other objects.
    #
    # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
    module ClassMethods
      # Returns an array of AggregateReflection objects for all the aggregations in the class.
      def reflect_on_all_aggregations
        read_inheritable_attribute(:aggregations) or write_inheritable_attribute(:aggregations, [])
      end

      # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
      #   Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
      def reflect_on_aggregation(aggregation)
        reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
      end

      # Returns an array of AssociationReflection objects for all the aggregations in the class.
      def reflect_on_all_associations
        read_inheritable_attribute(:associations) or write_inheritable_attribute(:associations, [])
      end

      # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
      #   Account.reflect_on_association(:owner) # returns the owner AssociationReflection
      def reflect_on_association(association)
        reflect_on_all_associations.find { |reflection| reflection.name == association }
      end
    end


    # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
    # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
    class MacroReflection
      attr_reader :active_record
      def initialize(macro, name, options, active_record)
        @macro, @name, @options, @active_record = macro, name, options, active_record
      end

      # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
      # :clients for "has_many :clients".
      def name
        @name
      end

      # Returns the name of the macro, so it would return :composed_of for
      # "composed_of :balance, :class_name => 'Money'" or :has_many for "has_many :clients".
      def macro
        @macro
      end

      # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
      # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
      def options
        @options
      end

      # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
      # "has_many :clients" would return the Client class.
      def klass() end

      def ==(other_aggregation)
        name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
      end
    end


    # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
    class AggregateReflection < MacroReflection #:nodoc:
      def klass
        Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
      end

      private
        def name_to_class_name(name)
          name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
        end
    end

    # Holds all the meta-data about an association as it was specified in the Active Record class.
    class AssociationReflection < MacroReflection #:nodoc:
      def klass
        @klass ||= active_record.send(:compute_type, (name_to_class_name(name.id2name)))
      end

      def table_name
        @table_name ||= klass.table_name
      end

      private
        def name_to_class_name(name)
          if name =~ /::/
            name
          else
            unless class_name = options[:class_name]
              class_name = name.to_s.camelize
              class_name = class_name.singularize if [:has_many, :has_and_belongs_to_many].include?(macro)
            end
            active_record.send(:type_name_with_module, class_name)
          end
        end
    end
  end
end