base.rb 15.0 KB
Newer Older
1
require 'active_resource/connection'
2 3
require 'cgi'
require 'set'
4 5 6

module ActiveResource
  class Base
7
    # The logger for diagnosing and tracing ARes calls.
8 9
    cattr_accessor :logger

10
    class << self
11
      # Gets the URI of the resource's site
12 13 14 15 16 17 18
      def site
        if defined?(@site)
          @site
        elsif superclass != Object and superclass.site
          superclass.site.dup.freeze
        end
      end
J
Jeremy Kemper 已提交
19

20
      # Set the URI for the REST resources
21
      def site=(site)
22
        @connection = nil
23
        @site = create_site_uri_from(site)
24 25
      end

26
      # Base connection to remote service
27 28 29 30
      def connection(refresh = false)
        @connection = Connection.new(site) if refresh || @connection.nil?
        @connection
      end
31

32 33
      def headers
        @headers ||= {}
34 35
      end

36 37 38 39
      # Do not include any modules in the default element name. This makes it easier to seclude ARes objects
      # in a separate namespace without having to set element_name repeatedly.
      attr_accessor_with_default(:element_name)    { to_s.split("::").last.underscore } #:nodoc:

40 41 42 43 44
      attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
      attr_accessor_with_default(:primary_key, 'id') #:nodoc:
      
      # Gets the resource prefix
      #  prefix/collectionname/1.xml
45 46 47
      def prefix(options={})
        default = site.path
        default << '/' unless default[-1..-1] == '/'
48
        # generate the actual method based on the current site path
49
        self.prefix = default
50 51
        prefix(options)
      end
52

53 54 55 56 57
      def prefix_source
        prefix # generate #prefix and #prefix_source methods first
        prefix_source
      end

58 59
      # Sets the resource prefix
      #  prefix/collectionname/1.xml
60
      def prefix=(value = '/')
61
        # Replace :placeholders with '#{embedded options[:lookups]}'
62
        prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
63 64 65

        # Redefine the new methods.
        code = <<-end_code
66 67
          def prefix_source() "#{value}" end
          def prefix(options={}) "#{prefix_call}" end
68 69
        end_code
        silence_warnings { instance_eval code, __FILE__, __LINE__ }
70
      rescue
71
        logger.error "Couldn't set prefix: #{$!}\n  #{code}"
72
        raise
73
      end
74

75
      alias_method :set_prefix, :prefix=  #:nodoc:
76

77 78
      alias_method :set_element_name, :element_name=  #:nodoc:
      alias_method :set_collection_name, :collection_name=  #:nodoc:
79

80 81 82 83 84 85 86 87 88
      # Gets the element path for the given ID.  If no query_options are given, they are split from the prefix options:
      #
      # Post.element_path(1) # => /posts/1.xml
      # Comment.element_path(1, :post_id => 5) # => /posts/5/comments/1.xml
      # Comment.element_path(1, :post_id => 5, :active => 1) # => /posts/5/comments/1.xml?active=1
      # Comment.element_path(1, {:post_id => 5}, {:active => 1}) # => /posts/5/comments/1.xml?active=1
      def element_path(id, prefix_options = {}, query_options = nil)
        prefix_options, query_options = split_options(prefix_options) if query_options.nil?
        "#{prefix(prefix_options)}#{collection_name}/#{id}.xml#{query_string(query_options)}"
89
      end
90

91 92 93 94 95 96 97 98 99
      # Gets the collection path.  If no query_options are given, they are split from the prefix options:
      #
      # Post.collection_path # => /posts.xml
      # Comment.collection_path(:post_id => 5) # => /posts/5/comments.xml
      # Comment.collection_path(:post_id => 5, :active => 1) # => /posts/5/comments.xml?active=1
      # Comment.collection_path({:post_id => 5}, {:active => 1}) # => /posts/5/comments.xml?active=1
      def collection_path(prefix_options = {}, query_options = nil)
        prefix_options, query_options = split_options(prefix_options) if query_options.nil?
        "#{prefix(prefix_options)}#{collection_name}.xml#{query_string(query_options)}"
100
      end
101

102
      alias_method :set_primary_key, :primary_key=  #:nodoc:
103

104 105 106 107 108 109 110 111 112 113 114
      # Create a new resource instance and request to the remote service
      # that it be saved.  This is equivalent to the following simultaneous calls:
      #
      #   ryan = Person.new(:first => 'ryan')
      #   ryan.save
      #
      # The newly created resource is returned.  If a failure has occurred an
      # exception will be raised (see save).  If the resource is invalid and
      # has not been saved then <tt>resource.valid?</tt> will return <tt>false</tt>,
      # while <tt>resource.new?</tt> will still return <tt>true</tt>.
      #      
115 116
      def create(attributes = {})
        returning(self.new(attributes)) { |res| res.save }        
117 118
      end

119
      # Core method for finding resources.  Used similarly to Active Record's find method.
120 121 122 123 124 125 126 127 128
      #
      #   Person.find(1)                                         # => GET /people/1.xml
      #   Person.find(:all)                                      # => GET /people.xml
      #   Person.find(:all, :params => { :title => "CEO" })      # => GET /people.xml?title=CEO
      #   Person.find(:all, :from => :managers)                  # => GET /people/managers.xml
      #   Person.find(:all, :from => "/companies/1/people.xml")  # => GET /companies/1/people.xml
      #   Person.find(:one, :from => :leader)                    # => GET /people/leader.xml
      #   Person.find(:one, :from => "/companies/1/manager.xml") # => GET /companies/1/manager.xml
      #   StreetAddress.find(1, :params => { :person_id => 1 })  # => GET /people/1/street_addresses/1.xml
129
      def find(*arguments)
130 131
        scope   = arguments.slice!(0)
        options = arguments.slice!(0) || {}
132 133

        case scope
134 135
          when :all   then find_every(options)
          when :first then find_every(options).first
136
          when :one   then find_one(options)
137
          else             find_single(scope, options)
138 139
        end
      end
140

141 142
      def delete(id, options = {})
        connection.delete(element_path(id, options))
143 144
      end

145
      # Evalutes to <tt>true</tt> if the resource is found.
146 147 148 149 150 151
      def exists?(id, options = {})
        id && !find_single(id, options).nil?
      rescue ActiveResource::ResourceNotFound
        false
      end

152
      private
153
        # Find every resource
154
        def find_every(options)
155
          case from = options[:from]
156 157 158 159 160 161 162 163
          when Symbol
            instantiate_collection(get(from, options[:params]))
          when String
            path = "#{from}#{query_string(options[:params])}"
            instantiate_collection(connection.get(path, headers) || [])
          else
            prefix_options, query_options = split_options(options[:params])
            path = collection_path(prefix_options, query_options)
164
            instantiate_collection( (connection.get(path, headers) || []), prefix_options )
165
          end
166 167
        end
        
168 169 170 171 172 173 174 175
        # Find a single resource from a one-off URL
        def find_one(options)
          case from = options[:from]
          when Symbol
            instantiate_record(get(from, options[:params]))
          when String
            path = "#{from}#{query_string(options[:params])}"
            instantiate_record(connection.get(path, headers))
176
          end
177
        end
178

179
        # Find a single resource from the default URL
180
        def find_single(scope, options)
181 182 183 184 185 186 187 188
          prefix_options, query_options = split_options(options[:params])
          path = element_path(scope, prefix_options, query_options)
          instantiate_record(connection.get(path, headers), prefix_options)
        end
        
        def instantiate_collection(collection, prefix_options = {})
          collection.collect! { |record| instantiate_record(record, prefix_options) }
        end
189

190 191
        def instantiate_record(record, prefix_options = {})
          returning new(record) do |resource|
192 193
            resource.prefix_options = prefix_options
          end
194
        end
195

196

197
        # Accepts a URI and creates the site URI from that.
198
        def create_site_uri_from(site)
199
          site.is_a?(URI) ? site.dup : URI.parse(site)
200
        end
201

202
        # contains a set of the current prefix parameters.
203 204 205 206
        def prefix_parameters
          @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
        end

207
        # Builds the query string for the request.
208
        def query_string(options)
209
          "?#{options.to_query}" unless options.nil? || options.empty? 
210 211 212 213 214
        end

        # split an option hash into two hashes, one containing the prefix options, 
        # and the other containing the leftovers.
        def split_options(options = {})
215 216 217
          prefix_options, query_options = {}, {}

          (options || {}).each do |key, value|
218 219
            next if key.blank?
            (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
220
          end
221 222

          [ prefix_options, query_options ]
223
        end
224 225
    end

226 227
    attr_accessor :attributes #:nodoc:
    attr_accessor :prefix_options #:nodoc:
228

229 230 231
    def initialize(attributes = {})
      @attributes     = {}
      @prefix_options = {}
232
      load(attributes)
233
    end
234

235
    # Is the resource a new object?
236
    def new?
237 238 239
      id.nil?
    end

240
    # Get the id of the object.
241
    def id
242
      attributes[self.class.primary_key]
243
    end
244

245
    # Set the id of the object.
246
    def id=(id)
247
      attributes[self.class.primary_key] = id
248
    end
249

250
    # 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+.
251 252 253 254 255 256 257 258 259 260 261 262 263 264
    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
265 266 267 268 269 270 271
    
    def dup
      returning new do |resource|
        resource.attributes     = @attributes
        resource.prefix_options = @prefix_options
      end
    end
272

273 274 275
    # Delegates to +create+ if a new object, +update+ if its old. If the response to the save includes a body,
    # it will be assumed that this body is XML for the final object as it looked after the save (which would include
    # attributes like created_at that wasn't part of the original submit).
276
    def save
277
      new? ? create : update
278 279
    end

280
    # Delete the resource.
281
    def destroy
282
      connection.delete(element_path, self.class.headers)
283
    end
284

285
    # Evaluates to <tt>true</tt> if this resource is found.
286
    def exists?
287
      !new? && self.class.exists?(id, :params => prefix_options)
288 289
    end

290
    # Convert the resource to an XML string
291 292
    def to_xml(options={})
      attributes.to_xml({:root => self.class.element_name}.merge(options))
293
    end
294 295 296

    # Reloads the attributes of this object from the remote web service.
    def reload
297
      self.load(self.class.find(id, :params => @prefix_options).attributes)
298 299 300 301 302
    end

    # Manually load attributes from a hash. Recursively loads collections of
    # resources.
    def load(attributes)
J
Jeremy Kemper 已提交
303
      raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
304
      @prefix_options, attributes = split_options(attributes)
305 306 307 308 309 310 311
      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
312 313
              resource = find_or_create_resource_for(key)
              resource.new(value)
314 315 316 317
            else
              value.dup rescue value
          end
      end
318 319
      self
    end
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    
    # For checking respond_to? without searching the attributes (which is faster).
    alias_method :respond_to_without_attributes?, :respond_to?

    # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
    # person.respond_to?("name?") which will all return true.
    def respond_to?(method, include_priv = false)
      method_name = method.to_s
      if attributes.nil?
        return super
      elsif attributes.has_key?(method_name)
        return true 
      elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
        return true
      end
      # super must be called at the end of the method, because the inherited respond_to?
      # would return true for generated readers, even if the attribute wasn't present
      super
    end
    
340

341 342 343 344
    protected
      def connection(refresh = false)
        self.class.connection(refresh)
      end
345

346
      # Update the resource on the remote service.
347
      def update
348
        returning connection.put(element_path(prefix_options), to_xml, self.class.headers) do |response|
349 350
          load_attributes_from_response(response)
        end
351
      end
352

353
      # Create (i.e., save to the remote service) the new resource.
354
      def create
355
        returning connection.post(collection_path, to_xml, self.class.headers) do |response|
356
          self.id = id_from_response(response)
357
          load_attributes_from_response(response)
358
        end
359
      end
360 361 362 363
      
      def load_attributes_from_response(response)
        if response['Content-size'] != "0" && response.body.strip.size > 0
          load(connection.xml_from_response(response))
364
        end
365
      end
366

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

372 373 374 375 376 377 378 379
      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

380
    private
381
      # Tries to find a resource for a given collection name; if it fails, then the resource is created
382 383 384
      def find_or_create_resource_for_collection(name)
        find_or_create_resource_for(name.to_s.singularize)
      end
385 386
      
      # Tries to find a resource for a given name; if it fails, then the resource is created
387 388
      def find_or_create_resource_for(name)
        resource_name = name.to_s.camelize
389 390 391

        # FIXME: Make it generic enough to support any depth of module nesting
        if (ancestors = self.class.name.split("::")).size > 1
392 393 394 395 396
          begin
            ancestors.first.constantize.const_get(resource_name)
          rescue NameError
            self.class.const_get(resource_name)
          end
397 398 399
        else
          self.class.const_get(resource_name)
        end
400 401 402
      rescue NameError
        resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
        resource.prefix = self.class.prefix
403
        resource.site   = self.class.site
404 405 406
        resource
      end

407 408 409 410
      def split_options(options = {})
        self.class.send(:split_options, options)
      end

411
      def method_missing(method_symbol, *arguments) #:nodoc:
412
        method_name = method_symbol.to_s
413

414 415 416 417
        case method_name.last
          when "="
            attributes[method_name.first(-1)] = arguments.first
          when "?"
T
 
Tobias Lütke 已提交
418
            attributes[method_name.first(-1)]
419
          else
420
            attributes.has_key?(method_name) ? attributes[method_name] : super
421 422 423
        end
      end
  end
J
Jeremy Kemper 已提交
424
end