Froze rails gems
[depot.git] / vendor / rails / activerecord / lib / active_record / associations / association_proxy.rb
diff --git a/vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb b/vendor/rails/activerecord/lib/active_record/associations/association_proxy.rb
new file mode 100644 (file)
index 0000000..d1a79df
--- /dev/null
@@ -0,0 +1,272 @@
+module ActiveRecord
+  module Associations
+    # This is the root class of all association proxies:
+    #
+    #   AssociationProxy
+    #     BelongsToAssociation
+    #       HasOneAssociation
+    #     BelongsToPolymorphicAssociation
+    #     AssociationCollection
+    #       HasAndBelongsToManyAssociation
+    #       HasManyAssociation
+    #         HasManyThroughAssociation
+    #            HasOneThroughAssociation
+    #
+    # Association proxies in Active Record are middlemen between the object that
+    # holds the association, known as the <tt>@owner</tt>, and the actual associated
+    # object, known as the <tt>@target</tt>. The kind of association any proxy is
+    # about is available in <tt>@reflection</tt>. That's an instance of the class
+    # ActiveRecord::Reflection::AssociationReflection.
+    #
+    # For example, given
+    #
+    #   class Blog < ActiveRecord::Base
+    #     has_many :posts
+    #   end
+    #
+    #   blog = Blog.find(:first)
+    #
+    # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
+    # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
+    # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
+    #
+    # This class has most of the basic instance methods removed, and delegates
+    # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
+    # corner case, it even removes the +class+ method and that's why you get
+    #
+    #   blog.posts.class # => Array
+    #
+    # though the object behind <tt>blog.posts</tt> is not an Array, but an
+    # ActiveRecord::Associations::HasManyAssociation.
+    #
+    # The <tt>@target</tt> object is not \loaded until needed. For example,
+    #
+    #   blog.posts.count
+    #
+    # is computed directly through SQL and does not trigger by itself the
+    # instantiation of the actual post records.
+    class AssociationProxy #:nodoc:
+      alias_method :proxy_respond_to?, :respond_to?
+      alias_method :proxy_extend, :extend
+      delegate :to_param, :to => :proxy_target
+      instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
+
+      def initialize(owner, reflection)
+        @owner, @reflection = owner, reflection
+        Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
+        reset
+      end
+
+      # Returns the owner of the proxy.
+      def proxy_owner
+        @owner
+      end
+
+      # Returns the reflection object that represents the association handled
+      # by the proxy.
+      def proxy_reflection
+        @reflection
+      end
+
+      # Returns the \target of the proxy, same as +target+.
+      def proxy_target
+        @target
+      end
+
+      # Does the proxy or its \target respond to +symbol+?
+      def respond_to?(*args)
+        proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
+      end
+
+      # Forwards <tt>===</tt> explicitly to the \target because the instance method
+      # removal above doesn't catch it. Loads the \target if needed.
+      def ===(other)
+        load_target
+        other === @target
+      end
+
+      # Returns the name of the table of the related class:
+      #
+      #   post.comments.aliased_table_name # => "comments"
+      #
+      def aliased_table_name
+        @reflection.klass.table_name
+      end
+
+      # Returns the SQL string that corresponds to the <tt>:conditions</tt>
+      # option of the macro, if given, or +nil+ otherwise.
+      def conditions
+        @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
+      end
+      alias :sql_conditions :conditions
+
+      # Resets the \loaded flag to +false+ and sets the \target to +nil+.
+      def reset
+        @loaded = false
+        @target = nil
+      end
+
+      # Reloads the \target and returns +self+ on success.
+      def reload
+        reset
+        load_target
+        self unless @target.nil?
+      end
+
+      # Has the \target been already \loaded?
+      def loaded?
+        @loaded
+      end
+
+      # Asserts the \target has been loaded setting the \loaded flag to +true+.
+      def loaded
+        @loaded = true
+      end
+
+      # Returns the target of this proxy, same as +proxy_target+.
+      def target
+        @target
+      end
+
+      # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
+      def target=(target)
+        @target = target
+        loaded
+      end
+
+      # Forwards the call to the target. Loads the \target if needed.
+      def inspect
+        load_target
+        @target.inspect
+      end
+
+      def send(method, *args)
+        if proxy_respond_to?(method)
+          super
+        else
+          load_target
+          @target.send(method, *args)
+        end
+      end
+
+      protected
+        # Does the association have a <tt>:dependent</tt> option?
+        def dependent?
+          @reflection.options[:dependent]
+        end
+
+        # Returns a string with the IDs of +records+ joined with a comma, quoted
+        # if needed. The result is ready to be inserted into a SQL IN clause.
+        #
+        #   quoted_record_ids(records) # => "23,56,58,67"
+        #
+        def quoted_record_ids(records)
+          records.map { |record| record.quoted_id }.join(',')
+        end
+
+        def interpolate_sql(sql, record = nil)
+          @owner.send(:interpolate_sql, sql, record)
+        end
+
+        # Forwards the call to the reflection class.
+        def sanitize_sql(sql)
+          @reflection.klass.send(:sanitize_sql, sql)
+        end
+
+        # Assigns the ID of the owner to the corresponding foreign key in +record+.
+        # If the association is polymorphic the type of the owner is also set.
+        def set_belongs_to_association_for(record)
+          if @reflection.options[:as]
+            record["#{@reflection.options[:as]}_id"]   = @owner.id unless @owner.new_record?
+            record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
+          else
+            record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
+          end
+        end
+
+        # Merges into +options+ the ones coming from the reflection.
+        def merge_options_from_reflection!(options)
+          options.reverse_merge!(
+            :group   => @reflection.options[:group],
+            :limit   => @reflection.options[:limit],
+            :offset  => @reflection.options[:offset],
+            :joins   => @reflection.options[:joins],
+            :include => @reflection.options[:include],
+            :select  => @reflection.options[:select],
+            :readonly  => @reflection.options[:readonly]
+          )
+        end
+
+        # Forwards +with_scope+ to the reflection.
+        def with_scope(*args, &block)
+          @reflection.klass.send :with_scope, *args, &block
+        end
+
+      private
+        # Forwards any missing method call to the \target.
+        def method_missing(method, *args)
+          if load_target
+            raise NoMethodError unless @target.respond_to?(method)
+
+            if block_given?
+              @target.send(method, *args)  { |*block_args| yield(*block_args) }
+            else
+              @target.send(method, *args)
+            end
+          end
+        end
+
+        # Loads the \target if needed and returns it.
+        #
+        # This method is abstract in the sense that it relies on +find_target+,
+        # which is expected to be provided by descendants.
+        #
+        # If the \target is already \loaded it is just returned. Thus, you can call
+        # +load_target+ unconditionally to get the \target.
+        #
+        # ActiveRecord::RecordNotFound is rescued within the method, and it is
+        # not reraised. The proxy is \reset and +nil+ is the return value.
+        def load_target
+          return nil unless defined?(@loaded)
+
+          if !loaded? and (!@owner.new_record? || foreign_key_present)
+            @target = find_target
+          end
+
+          @loaded = true
+          @target
+        rescue ActiveRecord::RecordNotFound
+          reset
+        end
+
+        # Can be overwritten by associations that might have the foreign key
+        # available for an association without having the object itself (and
+        # still being a new record). Currently, only +belongs_to+ presents
+        # this scenario (both vanilla and polymorphic).
+        def foreign_key_present
+          false
+        end
+
+        # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
+        # the kind of the class of the associated objects. Meant to be used as
+        # a sanity check when you are about to assign an associated record.
+        def raise_on_type_mismatch(record)
+          unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
+            message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
+            raise ActiveRecord::AssociationTypeMismatch, message
+          end
+        end
+
+        # Array#flatten has problems with recursive arrays. Going one level
+        # deeper solves the majority of the problems.
+        def flatten_deeper(array)
+          array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
+        end
+
+        # Returns the ID of the owner, quoted if needed.
+        def owner_quoted_id
+          @owner.quoted_id
+        end
+    end
+  end
+end