aboutsummaryrefslogblamecommitdiffstats
path: root/activeresource/lib/active_resource/base.rb
blob: c8225bba1b4837296c35be047e7d9ec45b3e55b8 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                    

             


                     



                                                                         
                 






                                                      
 
                     
                                          

                         





                                                                         
 
                                                                      

                                                                             
 


                                                    
                             

                       
 
                              









                                                                    
         
 
                                        
 
                                                    
                                                          

                                        
                                                                                
         
 
                                       
                                                                          
         
 
                                                  
 

                                                                                        
                          

                                           

                  


                                                      

           
 



                                           
             
                               

                                                                     
           
 

                                       
                                                                    
           
 
                                      
                                                      
           


                                                         
                                                    


















                                                                                         


                             
                                 
 
                                                        

                          
                                      
       
 
            


             
          
                                        
       
 
               
                                             
       
 















                                                                                                                           
            
                            


               
                                     
       
 

                                                                          
       


                                                                        





                                                                            
                                                                                                                 






                                                                    

                                                         





                                               


          



                                      
 
                
                                            
            
         

                
                                                       

                                        

         




                                                                        







                                                              










                                                                                       
                                         


                

                                                   
 



                                                               
                                                     
              
                                                                              


           
   
require 'active_resource/connection'
require 'cgi'
require 'set'

module ActiveResource
  class Base
    # The logger for logging diagnostic and trace information during ARes
    # calls.
    cattr_accessor :logger

    class << self
      def site
        if defined?(@site)
          @site
        elsif superclass != Object and superclass.site
          superclass.site.dup.freeze
        end
      end

      def site=(site)
        @site = create_site_uri_from(site)
        @connection = nil
        @site
      end

      def connection(refresh = false)
        @connection = Connection.new(site) if refresh || @connection.nil?
        @connection
      end

      attr_accessor_with_default(:element_name)    { to_s.underscore }
      attr_accessor_with_default(:collection_name) { element_name.pluralize }
      attr_accessor_with_default(:primary_key, 'id')

      def prefix(options={})
        default = site.path
        default << '/' unless default[-1..-1] == '/'
        self.prefix = default
        prefix(options)
      end

      def prefix=(value = '/')
        @prefix_parameters = Set.new
        prefix_call = value.gsub(/:\w+/) do |key|
          @prefix_parameters << key[1..-1].to_sym
          "\#{options[#{key}]}"
        end
        method_decl = %(def prefix(options={}) "#{prefix_call}" end)
        instance_eval method_decl, __FILE__, __LINE__
      rescue
        logger.error "Couldn't set prefix: #{$!}\n  #{method_decl}"
        raise
      end

      alias_method :set_prefix, :prefix=

      alias_method :set_element_name, :element_name=
      alias_method :set_collection_name, :collection_name=

      def element_path(id, options = {})
        "#{prefix(options)}#{collection_name}/#{id}.xml#{query_string(options)}"
      end

      def collection_path(options = {})
        "#{prefix(options)}#{collection_name}.xml#{query_string(options)}"
      end

      alias_method :set_primary_key, :primary_key=

      # Person.find(1) # => GET /people/1.xml
      # StreetAddress.find(1, :person_id => 1) # => GET /people/1/street_addresses/1.xml
      def find(*arguments)
        scope   = arguments.slice!(0)
        options = arguments.slice!(0) || {}

        case scope
          when :all   then find_every(options)
          when :first then find_every(options).first
          else             find_single(scope, options)
        end
      end

      def delete(id)
        connection.delete(element_path(id))
      end

      private
        def find_every(options)
          collection = connection.get(collection_path(options)) || []
          collection.collect! { |element| new(element, options) }
        end

        # { :person => person1 }
        def find_single(scope, options)
          new(connection.get(element_path(scope, options)), options)
        end

        def create_site_uri_from(site)
          site.is_a?(URI) ? site.dup : URI.parse(site)
        end

        def query_string(options)
          # Omit parameters which appear in the URI path.
          prefix unless defined?(@prefix_parameters)
          query_params = options.reject { |key, value| @prefix_parameters.include?(key) }

          # Accumulate a list of escaped key=value pairs for the given parameters.
          pairs = []
          query_params.each do |key, value|
            key = CGI.escape(key.to_s)

            # a => b becomes a=b
            # a => [b, c] becomes a[]=b&a[]=c
            case value
              when Array
                value.each { |val| pairs << "#{key}[]=#{CGI.escape(val.to_s)}" }
              else
                pairs << "#{key}=#{CGI.escape(value.to_s)}"
            end
          end

          "?#{pairs * '&'}" unless pairs.empty?
        end
    end

    attr_accessor :attributes
    attr_accessor :prefix_options

    def initialize(attributes = {}, prefix_options = {})
      @attributes = {}
      self.load attributes
      @prefix_options = prefix_options
    end

    def new?
      id.nil?
    end

    def id
      attributes[self.class.primary_key]
    end

    def id=(id)
      attributes[self.class.primary_key] = id
    end

    # True if and only if +other+ is the same object or is an instance of the same class, is not new?, and has the same id.
    def ==(other)
      other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
    end

    # Delegates to ==
    def eql?(other)
      self == other
    end

    # Delegates to id in order to allow two resources of the same type and id to work with something like:
    #   [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
    def hash
      id.hash
    end

    def save
      new? ? create : update
    end

    def destroy
      connection.delete(element_path)
    end

    def to_xml(options={})
      attributes.to_xml({:root => self.class.element_name}.merge(options))
    end

    # Reloads the attributes of this object from the remote web service.
    def reload
      self.load self.class.find(id, @prefix_options).attributes
    end

    # Manually load attributes from a hash. Recursively loads collections of
    # resources.
    def load(attributes)
      raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
      attributes.each do |key, value|
        @attributes[key.to_s] =
          case value
            when Array
              resource = find_or_create_resource_for_collection(key)
              value.map { |attrs| resource.new(attrs) }
            when Hash
              resource = find_or_create_resource_for(key)
              resource.new(value)
            when ActiveResource::Base
              value.class.new(value.attributes)
            else
              value.dup rescue value
          end
      end
      self
    end

    protected
      def connection(refresh = false)
        self.class.connection(refresh)
      end

      def update
        connection.put(element_path, to_xml)
        true
      end

      def create
        resp = connection.post(collection_path, to_xml)
        self.id = id_from_response(resp)
        true
      end

      # takes a response from a typical create post and pulls the ID out
      def id_from_response(response)
        response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
      end

      def element_path(options = nil)
        self.class.element_path(id, options || prefix_options)
      end

      def collection_path(options = nil)
        self.class.collection_path(options || prefix_options)
      end

    private
      def find_or_create_resource_for_collection(name)
        find_or_create_resource_for(name.to_s.singularize)
      end

      def find_or_create_resource_for(name)
        resource_name = name.to_s.camelize
        resource_name.constantize
      rescue NameError
        resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
        resource.prefix = self.class.prefix
        resource.site   = self.class.site
        resource
      end

      def method_missing(method_symbol, *arguments)
        method_name = method_symbol.to_s

        case method_name.last
          when "="
            attributes[method_name.first(-1)] = arguments.first
          when "?"
            attributes[method_name.first(-1)] == true
          else
            attributes.has_key?(method_name) ? attributes[method_name] : super
        end
      end
  end
end