base.rb 14.1 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 164 165
          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)
            instantiate_collection(connection.get(path, headers) || [])
          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
            (prefix_parameters.include?(key) ? prefix_options : query_options)[key] = value
          end
220 221

          [ prefix_options, query_options ]
222
        end
223 224
    end

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

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

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

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

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

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

272 273 274
    # 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).
275
    def save
276
      new? ? create : update
277 278
    end

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

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

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

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

    # Manually load attributes from a hash. Recursively loads collections of
    # resources.
    def load(attributes)
J
Jeremy Kemper 已提交
302
      raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
303
      @prefix_options, attributes = split_options(attributes)
304 305 306 307 308 309 310
      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
311 312
              resource = find_or_create_resource_for(key)
              resource.new(value)
313 314 315 316
            else
              value.dup rescue value
          end
      end
317 318 319
      self
    end

320 321 322 323
    protected
      def connection(refresh = false)
        self.class.connection(refresh)
      end
324

325
      # Update the resource on the remote service.
326
      def update
327
        returning connection.put(element_path(prefix_options), to_xml, self.class.headers) do |response|
328 329
          load_attributes_from_response(response)
        end
330
      end
331

332
      # Create (i.e., save to the remote service) the new resource.
333
      def create
334
        returning connection.post(collection_path, to_xml, self.class.headers) do |response|
335
          self.id = id_from_response(response)
336
          load_attributes_from_response(response)
337
        end
338
      end
339 340 341 342 343 344
      
      def load_attributes_from_response(response)
        if response['Content-size'] != "0" && response.body.strip.size > 0
          load(connection.xml_from_response(response))
        end        
      end
345

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

351 352 353 354 355 356 357 358
      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

359
    private
360
      # Tries to find a resource for a given collection name; if it fails, then the resource is created
361 362 363
      def find_or_create_resource_for_collection(name)
        find_or_create_resource_for(name.to_s.singularize)
      end
364 365
      
      # Tries to find a resource for a given name; if it fails, then the resource is created
366 367
      def find_or_create_resource_for(name)
        resource_name = name.to_s.camelize
368 369 370

        # FIXME: Make it generic enough to support any depth of module nesting
        if (ancestors = self.class.name.split("::")).size > 1
371 372 373 374 375
          begin
            ancestors.first.constantize.const_get(resource_name)
          rescue NameError
            self.class.const_get(resource_name)
          end
376 377 378
        else
          self.class.const_get(resource_name)
        end
379 380 381
      rescue NameError
        resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
        resource.prefix = self.class.prefix
382
        resource.site   = self.class.site
383 384 385
        resource
      end

386 387 388 389
      def split_options(options = {})
        self.class.send(:split_options, options)
      end

390
      def method_missing(method_symbol, *arguments) #:nodoc:
391
        method_name = method_symbol.to_s
392

393 394 395 396
        case method_name.last
          when "="
            attributes[method_name.first(-1)] = arguments.first
          when "?"
397
            attributes[method_name.first(-1)] == true
398
          else
399
            attributes.has_key?(method_name) ? attributes[method_name] : super
400 401 402
        end
      end
  end
J
Jeremy Kemper 已提交
403
end