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

module ActiveResource
6 7
  # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
  #
8
  # For an outline of what Active Resource is capable of, see link:files/vendor/rails/activeresource/README.html.
9 10 11 12 13 14 15
  #
  # == Automated mapping
  #
  # Active Resource objects represent your RESTful resources as manipulatable Ruby objects.  To map resources
  # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
  # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
  # URI of the resources.
16
  #
P
Pratik Naik 已提交
17 18 19
  #   class Person < ActiveResource::Base
  #     self.site = "http://api.people.com:3000/"
  #   end
20
  #
21
  # Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and
22
  # you can now use Active Resource's lifecycles methods to manipulate resources. In the case where you already have
P
Pratik Naik 已提交
23 24 25 26 27 28
  # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value.
  #
  #   class PersonResource < ActiveResource::Base
  #     self.site = "http://api.people.com:3000/"
  #     self.element_name = "person"
  #   end
29 30
  #
  #
31 32 33 34
  # == Lifecycle methods
  #
  # Active Resource exposes methods for creating, finding, updating, and deleting resources
  # from REST web services.
35
  #
36
  #   ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
P
Pratik Naik 已提交
37 38 39 40
  #   ryan.save                # => true
  #   ryan.id                  # => 2
  #   Person.exists?(ryan.id)  # => true
  #   ryan.exists?             # => true
41
  #
42
  #   ryan = Person.find(1)
P
Pratik Naik 已提交
43
  #   # Resource holding our newly created Person object
44
  #
45
  #   ryan.first = 'Rizzle'
P
Pratik Naik 已提交
46
  #   ryan.save                # => true
47
  #
P
Pratik Naik 已提交
48
  #   ryan.destroy             # => true
49 50 51
  #
  # As you can see, these are very similar to Active Record's lifecycle methods for database records.
  # You can read more about each of these methods in their respective documentation.
52
  #
53 54 55
  # === Custom REST methods
  #
  # Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
56
  # defining your own custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>,
P
Pratik Naik 已提交
57
  # <tt>post</tt>, <tt>put</tt> and <tt>\delete</tt> methods where you can specify a custom REST method
58 59 60 61
  # name to invoke.
  #
  #   # POST to the custom 'register' REST method, i.e. POST /people/new/register.xml.
  #   Person.new(:name => 'Ryan').post(:register)
62 63
  #   # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
  #
64
  #   # PUT an update by invoking the 'promote' REST method, i.e. PUT /people/1/promote.xml?position=Manager.
65
  #   Person.find(1).put(:promote, :position => 'Manager')
66
  #   # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
67 68 69 70 71 72 73
  #
  #   # GET all the positions available, i.e. GET /people/positions.xml.
  #   Person.get(:positions)
  #   # => [{:name => 'Manager'}, {:name => 'Clerk'}]
  #
  #   # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.xml.
  #   Person.find(1).delete(:fire)
74
  #
75
  # For more information on using custom REST methods, see the
76 77 78 79 80
  # ActiveResource::CustomMethods documentation.
  #
  # == Validations
  #
  # You can validate resources client side by overriding validation methods in the base class.
81
  #
P
Pratik Naik 已提交
82 83 84 85 86 87 88
  #   class Person < ActiveResource::Base
  #      self.site = "http://api.people.com:3000/"
  #      protected
  #        def validate
  #          errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
  #        end
  #   end
89
  #
90 91 92
  # See the ActiveResource::Validations documentation for more information.
  #
  # == Authentication
93
  #
94
  # Many REST APIs will require authentication, usually in the form of basic
95 96
  # HTTP authentication.  Authentication can be specified by:
  # * putting the credentials in the URL for the +site+ variable.
97
  #
98 99 100
  #    class Person < ActiveResource::Base
  #      self.site = "http://ryan:password@api.people.com:3000/"
  #    end
101
  #
102 103 104 105 106 107 108 109
  # * defining +user+ and/or +password+ variables
  #
  #    class Person < ActiveResource::Base
  #      self.site = "http://api.people.com:3000/"
  #      self.user = "ryan"
  #      self.password = "password"
  #    end
  #
110
  # For obvious security reasons, it is probably best if such services are available
111
  # over HTTPS.
112 113
  #
  # Note: Some values cannot be provided in the URL passed to site.  e.g. email addresses
P
Pratik Naik 已提交
114
  # as usernames.  In those situations you should use the separate user and password option.
115 116 117
  # == Errors & Validation
  #
  # Error handling and validation is handled in much the same manner as you're used to seeing in
118
  # Active Record.  Both the response code in the HTTP response and the body of the response are used to
119
  # indicate that an error occurred.
120
  #
121
  # === Resource errors
122
  #
123
  # When a GET is requested for a resource that does not exist, the HTTP <tt>404</tt> (Resource Not Found)
124 125
  # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
  # exception.
126
  #
127
  #   # GET http://api.people.com:3000/people/999.xml
P
Pratik Naik 已提交
128
  #   ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
129
  #
P
Pratik Naik 已提交
130
  # <tt>404</tt> is just one of the HTTP error response codes that Active Resource will handle with its own exception. The
131
  # following HTTP response codes will also result in these exceptions:
P
Pratik Naik 已提交
132 133 134 135 136 137
  # 
  # * 200..399 - Valid response, no exception (other than 301, 302)
  # * 301, 302 - ActiveResource::Redirection
  # * 400 - ActiveResource::BadRequest
  # * 401 - ActiveResource::UnauthorizedAccess
  # * 403 - ActiveResource::ForbiddenAccess
P
Pratik Naik 已提交
138
  # * 404 - ActiveResource::ResourceNotFound
P
Pratik Naik 已提交
139
  # * 405 - ActiveResource::MethodNotAllowed
P
Pratik Naik 已提交
140 141 142 143
  # * 409 - ActiveResource::ResourceConflict
  # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)
  # * 401..499 - ActiveResource::ClientError
  # * 500..599 - ActiveResource::ServerError
P
Pratik Naik 已提交
144
  # * Other - ActiveResource::ConnectionError
145 146 147 148 149 150 151 152 153 154 155 156 157
  #
  # These custom exceptions allow you to deal with resource errors more naturally and with more precision
  # rather than returning a general HTTP error.  For example:
  #
  #   begin
  #     ryan = Person.find(my_id)
  #   rescue ActiveResource::ResourceNotFound
  #     redirect_to :action => 'not_found'
  #   rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
  #     redirect_to :action => 'new'
  #   end
  #
  # === Validation errors
158
  #
159
  # Active Resource supports validations on resources and will return errors if any these validations fail
160
  # (e.g., "First name can not be blank" and so on).  These types of errors are denoted in the response by
161 162
  # a response code of <tt>422</tt> and an XML representation of the validation errors.  The save operation will
  # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
163
  #
164
  #   ryan = Person.find(1)
P
Pratik Naik 已提交
165 166
  #   ryan.first # => ''
  #   ryan.save  # => false
167
  #
168
  #   # When
169 170 171 172
  #   # PUT http://api.people.com:3000/people/1.xml
  #   # is requested with invalid values, the response is:
  #   #
  #   # Response (422):
173
  #   # <errors type="array"><error>First cannot be empty</error></errors>
174 175
  #   #
  #
P
Pratik Naik 已提交
176 177
  #   ryan.errors.invalid?(:first)  # => true
  #   ryan.errors.full_messages     # => ['First cannot be empty']
178
  #
179 180
  # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
  #
181 182 183
  # === Timeouts
  #
  # Active Resource relies on HTTP to access RESTful APIs and as such is inherently susceptible to slow or
P
Pratik Naik 已提交
184
  # unresponsive servers. In such cases, your Active Resource method calls could \timeout. You can control the
185 186 187 188 189 190 191
  # amount of time before Active Resource times out with the +timeout+ variable.
  #
  #   class Person < ActiveResource::Base
  #     self.site = "http://api.people.com:3000/"
  #     self.timeout = 5
  #   end
  #
P
Pratik Naik 已提交
192
  # This sets the +timeout+ to 5 seconds. You can adjust the +timeout+ to a value suitable for the RESTful API
193 194 195 196 197
  # you are accessing. It is recommended to set this to a reasonably low value to allow your Active Resource
  # clients (especially if you are using Active Resource in a Rails application) to fail-fast (see
  # http://en.wikipedia.org/wiki/Fail-fast) rather than cause cascading failures that could incapacitate your
  # server.
  #
P
Pratik Naik 已提交
198
  # When a \timeout occurs, an ActiveResource::TimeoutError is raised. You should rescue from
P
Pratik Naik 已提交
199 200
  # ActiveResource::TimeoutError in your Active Resource method calls.
  #
201 202 203
  # Internally, Active Resource relies on Ruby's Net::HTTP library to make HTTP requests. Setting +timeout+
  # sets the <tt>read_timeout</tt> of the internal Net::HTTP instance to the same value. The default
  # <tt>read_timeout</tt> is 60 seconds on most Ruby implementations.
204
  class Base
205
    # The logger for diagnosing and tracing Active Resource calls.
206 207
    cattr_accessor :logger

208
    class << self
P
Pratik Naik 已提交
209
      # Gets the URI of the REST resources to map for this class.  The site variable is required for
P
Pratik Naik 已提交
210
      # Active Resource's mapping to work.
211
      def site
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
        # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
        #
        # With superclass_delegating_reader
        #
        #   Parent.site = 'http://anonymous@test.com'
        #   Subclass.site # => 'http://anonymous@test.com'
        #   Subclass.site.user = 'david'
        #   Parent.site # => 'http://david@test.com'
        #
        # Without superclass_delegating_reader (expected behaviour)
        #
        #   Parent.site = 'http://anonymous@test.com'
        #   Subclass.site # => 'http://anonymous@test.com'
        #   Subclass.site.user = 'david' # => TypeError: can't modify frozen object
        #
227 228
        if defined?(@site)
          @site
229
        elsif superclass != Object && superclass.site
230 231 232
          superclass.site.dup.freeze
        end
      end
J
Jeremy Kemper 已提交
233

234
      # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
P
Pratik Naik 已提交
235
      # The site variable is required for Active Resource's mapping to work.
236
      def site=(site)
237
        @connection = nil
238 239 240 241
        if site.nil?
          @site = nil
        else
          @site = create_site_uri_from(site)
242 243
          @user = URI.decode(@site.user) if @site.user
          @password = URI.decode(@site.password) if @site.password
244 245 246
        end
      end

P
Pratik Naik 已提交
247
      # Gets the \user for REST HTTP authentication.
248 249 250 251 252 253 254 255 256
      def user
        # Not using superclass_delegating_reader. See +site+ for explanation
        if defined?(@user)
          @user
        elsif superclass != Object && superclass.user
          superclass.user.dup.freeze
        end
      end

P
Pratik Naik 已提交
257
      # Sets the \user for REST HTTP authentication.
258 259 260 261 262
      def user=(user)
        @connection = nil
        @user = user
      end

P
Pratik Naik 已提交
263
      # Gets the \password for REST HTTP authentication.
264 265 266 267 268 269 270 271 272
      def password
        # Not using superclass_delegating_reader. See +site+ for explanation
        if defined?(@password)
          @password
        elsif superclass != Object && superclass.password
          superclass.password.dup.freeze
        end
      end

P
Pratik Naik 已提交
273
      # Sets the \password for REST HTTP authentication.
274 275 276
      def password=(password)
        @connection = nil
        @password = password
277 278
      end

P
Pratik Naik 已提交
279
      # Sets the format that attributes are sent and received in from a mime type reference:
280 281 282 283 284 285 286
      #
      #   Person.format = :json
      #   Person.find(1) # => GET /people/1.json
      #
      #   Person.format = ActiveResource::Formats::XmlFormat
      #   Person.find(1) # => GET /people/1.xml
      #
P
Pratik Naik 已提交
287
      # Default format is <tt>:xml</tt>.
288
      def format=(mime_type_reference_or_format)
289
        format = mime_type_reference_or_format.is_a?(Symbol) ?
290 291
          ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format

292
        write_inheritable_attribute(:format, format)
293
        connection.format = format if site
294
      end
295

P
Pratik Naik 已提交
296
      # Returns the current format, default is ActiveResource::Formats::XmlFormat.
P
Pratik Naik 已提交
297
      def format
298
        read_inheritable_attribute(:format) || ActiveResource::Formats[:xml]
299 300
      end

301 302 303 304 305 306
      # Sets the number of seconds after which requests to the REST API should time out.
      def timeout=(timeout)
        @connection = nil
        @timeout = timeout
      end

P
Pratik Naik 已提交
307
      # Gets the number of seconds after which requests to the REST API should time out.
308 309 310 311 312 313 314 315
      def timeout
        if defined?(@timeout)
          @timeout
        elsif superclass != Object && superclass.timeout
          superclass.timeout
        end
      end

P
Pratik Naik 已提交
316 317
      # An instance of ActiveResource::Connection that is the base \connection to the remote service.
      # The +refresh+ parameter toggles whether or not the \connection is refreshed at every request
318
      # or not (defaults to <tt>false</tt>).
319
      def connection(refresh = false)
320 321
        if defined?(@connection) || superclass == Object
          @connection = Connection.new(site, format) if refresh || @connection.nil?
322 323
          @connection.user = user if user
          @connection.password = password if password
324
          @connection.timeout = timeout if timeout
325 326 327 328
          @connection
        else
          superclass.connection
        end
329
      end
330

331 332
      def headers
        @headers ||= {}
333 334
      end

335 336 337 338
      # 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:

339 340
      attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
      attr_accessor_with_default(:primary_key, 'id') #:nodoc:
P
Pratik Naik 已提交
341 342 343
      
      # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
      # This method is regenerated at runtime based on what the \prefix is set to.
344 345 346
      def prefix(options={})
        default = site.path
        default << '/' unless default[-1..-1] == '/'
347
        # generate the actual method based on the current site path
348
        self.prefix = default
349 350
        prefix(options)
      end
351

P
Pratik Naik 已提交
352 353
      # An attribute reader for the source string for the resource path \prefix.  This
      # method is regenerated at runtime based on what the \prefix is set to.
354 355 356 357 358
      def prefix_source
        prefix # generate #prefix and #prefix_source methods first
        prefix_source
      end

P
Pratik Naik 已提交
359
      # Sets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
360
      # Default value is <tt>site.path</tt>.
361
      def prefix=(value = '/')
362
        # Replace :placeholders with '#{embedded options[:lookups]}'
363
        prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
364

365 366 367
        # Clear prefix parameters in case they have been cached
        @prefix_parameters = nil

368 369
        # Redefine the new methods.
        code = <<-end_code
370 371
          def prefix_source() "#{value}" end
          def prefix(options={}) "#{prefix_call}" end
372 373
        end_code
        silence_warnings { instance_eval code, __FILE__, __LINE__ }
374
      rescue
375
        logger.error "Couldn't set prefix: #{$!}\n  #{code}"
376
        raise
377
      end
378

379
      alias_method :set_prefix, :prefix=  #:nodoc:
380

381 382
      alias_method :set_element_name, :element_name=  #:nodoc:
      alias_method :set_collection_name, :collection_name=  #:nodoc:
383

384
      # Gets the element path for the given ID in +id+.  If the +query_options+ parameter is omitted, Rails
P
Pratik Naik 已提交
385
      # will split from the \prefix options.
386 387
      #
      # ==== Options
P
Pratik Naik 已提交
388
      # +prefix_options+ - A \hash to add a \prefix to the request for nested URLs (e.g., <tt>:account_id => 19</tt>
389
      #                    would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
P
Pratik Naik 已提交
390
      # +query_options+ - A \hash to add items to the query string for the request.
391 392
      #
      # ==== Examples
393
      #   Post.element_path(1)
394 395
      #   # => /posts/1.xml
      #
396
      #   Comment.element_path(1, :post_id => 5)
397 398
      #   # => /posts/5/comments/1.xml
      #
399
      #   Comment.element_path(1, :post_id => 5, :active => 1)
400 401
      #   # => /posts/5/comments/1.xml?active=1
      #
402
      #   Comment.element_path(1, {:post_id => 5}, {:active => 1})
403
      #   # => /posts/5/comments/1.xml?active=1
404 405 406
      #
      def element_path(id, prefix_options = {}, query_options = nil)
        prefix_options, query_options = split_options(prefix_options) if query_options.nil?
407
        "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"
408
      end
409

410 411 412 413
      # Gets the collection path for the REST resources.  If the +query_options+ parameter is omitted, Rails
      # will split from the +prefix_options+.
      #
      # ==== Options
P
Pratik Naik 已提交
414 415 416
      # * +prefix_options+ - A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
      #   would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
      # * +query_options+ - A hash to add items to the query string for the request.
417 418 419 420 421
      #
      # ==== Examples
      #   Post.collection_path
      #   # => /posts.xml
      #
422
      #   Comment.collection_path(:post_id => 5)
423 424
      #   # => /posts/5/comments.xml
      #
425
      #   Comment.collection_path(:post_id => 5, :active => 1)
426 427
      #   # => /posts/5/comments.xml?active=1
      #
428
      #   Comment.collection_path({:post_id => 5}, {:active => 1})
429
      #   # => /posts/5/comments.xml?active=1
430 431 432
      #
      def collection_path(prefix_options = {}, query_options = nil)
        prefix_options, query_options = split_options(prefix_options) if query_options.nil?
433
        "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
434
      end
435

436
      alias_method :set_primary_key, :primary_key=  #:nodoc:
437

P
Pratik Naik 已提交
438
      # Creates a new resource instance and makes a request to the remote service
439
      # that it be saved, making it equivalent to the following simultaneous calls:
440 441 442 443
      #
      #   ryan = Person.new(:first => 'ryan')
      #   ryan.save
      #
P
Pratik Naik 已提交
444 445 446 447
      # Returns the newly created resource.  If a failure has occurred an
      # exception will be raised (see <tt>save</tt>).  If the resource is invalid and
      # has not been saved then <tt>valid?</tt> will return <tt>false</tt>,
      # while <tt>new?</tt> will still return <tt>true</tt>.
448 449 450 451
      #
      # ==== Examples
      #   Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
      #   my_person = Person.find(:first)
P
Pratik Naik 已提交
452
      #   my_person.email # => myname@nospam.com
453 454
      #
      #   dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
P
Pratik Naik 已提交
455 456
      #   dhh.valid? # => true
      #   dhh.new?   # => false
457 458 459
      #
      #   # We'll assume that there's a validation that requires the name attribute
      #   that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
P
Pratik Naik 已提交
460 461
      #   that_guy.valid? # => false
      #   that_guy.new?   # => true
462
      def create(attributes = {})
463
        returning(self.new(attributes)) { |res| res.save }
464 465
      end

P
Pratik Naik 已提交
466
      # Core method for finding resources.  Used similarly to Active Record's +find+ method.
467
      #
468
      # ==== Arguments
469
      # The first argument is considered to be the scope of the query.  That is, how many
470 471
      # resources are returned from the request.  It can be one of the following.
      #
P
Pratik Naik 已提交
472 473
      # * <tt>:one</tt> - Returns a single resource.
      # * <tt>:first</tt> - Returns the first resource found.
474
      # * <tt>:last</tt> - Returns the last resource found.
P
Pratik Naik 已提交
475
      # * <tt>:all</tt> - Returns every resource that matches the request.
476
      #
477
      # ==== Options
478
      #
P
Pratik Naik 已提交
479
      # * <tt>:from</tt> - Sets the path or custom method that resources will be fetched from.
P
Pratik Naik 已提交
480
      # * <tt>:params</tt> - Sets query and \prefix (nested URL) parameters.
481 482
      #
      # ==== Examples
483
      #   Person.find(1)
484 485
      #   # => GET /people/1.xml
      #
486
      #   Person.find(:all)
487 488
      #   # => GET /people.xml
      #
489
      #   Person.find(:all, :params => { :title => "CEO" })
490 491
      #   # => GET /people.xml?title=CEO
      #
492 493 494 495
      #   Person.find(:first, :from => :managers)
      #   # => GET /people/managers.xml
      #
      #   Person.find(:last, :from => :managers)
496 497
      #   # => GET /people/managers.xml
      #
498
      #   Person.find(:all, :from => "/companies/1/people.xml")
499 500
      #   # => GET /companies/1/people.xml
      #
501
      #   Person.find(:one, :from => :leader)
502 503
      #   # => GET /people/leader.xml
      #
504 505 506
      #   Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
      #   # => GET /people/developers.xml?language=ruby
      #
507
      #   Person.find(:one, :from => "/companies/1/manager.xml")
508 509
      #   # => GET /companies/1/manager.xml
      #
510
      #   StreetAddress.find(1, :params => { :person_id => 1 })
511
      #   # => GET /people/1/street_addresses/1.xml
512
      def find(*arguments)
513 514
        scope   = arguments.slice!(0)
        options = arguments.slice!(0) || {}
515 516

        case scope
517 518
          when :all   then find_every(options)
          when :first then find_every(options).first
519
          when :last  then find_every(options).last
520
          when :one   then find_one(options)
521
          else             find_single(scope, options)
522 523
        end
      end
524

525 526 527
      # Deletes the resources with the ID in the +id+ parameter.
      #
      # ==== Options
P
Pratik Naik 已提交
528
      # All options specify \prefix and query parameters.
529 530
      #
      # ==== Examples
P
Pratik Naik 已提交
531
      #   Event.delete(2) # sends DELETE /events/2
532 533
      #
      #   Event.create(:name => 'Free Concert', :location => 'Community Center')
P
Pratik Naik 已提交
534 535
      #   my_event = Event.find(:first) # let's assume this is event with ID 7
      #   Event.delete(my_event.id) # sends DELETE /events/7
536 537
      #
      #   # Let's assume a request to events/5/cancel.xml
P
Pratik Naik 已提交
538
      #   Event.delete(params[:id]) # sends DELETE /events/5
539 540
      def delete(id, options = {})
        connection.delete(element_path(id, options))
541 542
      end

543 544 545 546
      # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
      #
      # ==== Examples
      #   Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
P
Pratik Naik 已提交
547
      #   Note.exists?(1) # => true
548
      #
P
Pratik Naik 已提交
549
      #   Note.exists(1349) # => false
550
      def exists?(id, options = {})
551 552 553 554
        if id
          prefix_options, query_options = split_options(options[:params])
          path = element_path(id, prefix_options, query_options)
          response = connection.head(path, headers)
555
          response.code.to_i == 200
556 557
        end
        # id && !find_single(id, options).nil?
558 559 560 561
      rescue ActiveResource::ResourceNotFound
        false
      end

562
      private
563
        # Find every resource
564
        def find_every(options)
565
          case from = options[:from]
566 567 568 569 570 571 572 573
          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)
574
            instantiate_collection( (connection.get(path, headers) || []), prefix_options )
575
          end
576
        end
577

578 579 580 581 582 583 584 585
        # 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))
586
          end
587
        end
588

589
        # Find a single resource from the default URL
590
        def find_single(scope, options)
591 592 593 594
          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
595

596 597 598
        def instantiate_collection(collection, prefix_options = {})
          collection.collect! { |record| instantiate_record(record, prefix_options) }
        end
599

600 601
        def instantiate_record(record, prefix_options = {})
          returning new(record) do |resource|
602 603
            resource.prefix_options = prefix_options
          end
604
        end
605

606

607
        # Accepts a URI and creates the site URI from that.
608
        def create_site_uri_from(site)
609
          site.is_a?(URI) ? site.dup : URI.parse(site)
610
        end
611

612
        # contains a set of the current prefix parameters.
613 614 615 616
        def prefix_parameters
          @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
        end

617
        # Builds the query string for the request.
618
        def query_string(options)
619
          "?#{options.to_query}" unless options.nil? || options.empty?
620 621
        end

622
        # split an option hash into two hashes, one containing the prefix options,
623 624
        # and the other containing the leftovers.
        def split_options(options = {})
625 626 627
          prefix_options, query_options = {}, {}

          (options || {}).each do |key, value|
628 629
            next if key.blank?
            (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
630
          end
631 632

          [ prefix_options, query_options ]
633
        end
634 635
    end

636 637
    attr_accessor :attributes #:nodoc:
    attr_accessor :prefix_options #:nodoc:
638

P
Pratik Naik 已提交
639 640
    # Constructor method for \new resources; the optional +attributes+ parameter takes a \hash
    # of attributes for the \new resource.
641 642 643 644 645 646 647 648 649
    #
    # ==== Examples
    #   my_course = Course.new
    #   my_course.name = "Western Civilization"
    #   my_course.lecturer = "Don Trotter"
    #   my_course.save
    #
    #   my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
    #   my_other_course.save
650 651 652
    def initialize(attributes = {})
      @attributes     = {}
      @prefix_options = {}
653
      load(attributes)
654
    end
655

P
Pratik Naik 已提交
656 657
    # Returns a \clone of the resource that hasn't been assigned an +id+ yet and
    # is treated as a \new resource.
658
    #
P
Pratik Naik 已提交
659 660 661
    #   ryan = Person.find(1)
    #   not_ryan = ryan.clone
    #   not_ryan.new?  # => true
662 663
    #
    # Any active resource member attributes will NOT be cloned, though all other
P
Pratik Naik 已提交
664
    # attributes are.  This is to prevent the conflict between any +prefix_options+
665 666 667
    # that refer to the original parent resource and the newly cloned parent
    # resource that does not exist.
    #
P
Pratik Naik 已提交
668 669 670
    #   ryan = Person.find(1)
    #   ryan.address = StreetAddress.find(1, :person_id => ryan.id)
    #   ryan.hash = {:not => "an ARes instance"}
671
    #
P
Pratik Naik 已提交
672 673 674 675
    #   not_ryan = ryan.clone
    #   not_ryan.new?            # => true
    #   not_ryan.address         # => NoMethodError
    #   not_ryan.hash            # => {:not => "an ARes instance"}
676 677
    def clone
      # Clone all attributes except the pk and any nested ARes
J
Jeremy Kemper 已提交
678
      cloned = attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.inject({}) do |attrs, (k, v)|
679 680 681 682 683 684 685 686
        attrs[k] = v.clone
        attrs
      end
      # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
      # attempts to convert hashes into member objects and arrays into collections of objects.  We want
      # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
      resource = self.class.new({})
      resource.prefix_options = self.prefix_options
J
Jeremy Kemper 已提交
687
      resource.send :instance_variable_set, '@attributes', cloned
688 689 690 691
      resource
    end


P
Pratik Naik 已提交
692
    # A method to determine if the resource a \new object (i.e., it has not been POSTed to the remote service yet).
693 694 695
    #
    # ==== Examples
    #   not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
P
Pratik Naik 已提交
696
    #   not_new.new? # => false
697 698
    #
    #   is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
P
Pratik Naik 已提交
699
    #   is_new.new? # => true
700 701
    #
    #   is_new.save
P
Pratik Naik 已提交
702
    #   is_new.new? # => false
703
    #
704
    def new?
705 706
      id.nil?
    end
707
    alias :new_record? :new?
708

P
Pratik Naik 已提交
709
    # Gets the <tt>\id</tt> attribute of the resource.
710
    def id
711
      attributes[self.class.primary_key]
712
    end
713

P
Pratik Naik 已提交
714
    # Sets the <tt>\id</tt> attribute of the resource.
715
    def id=(id)
716
      attributes[self.class.primary_key] = id
717
    end
718

P
Pratik Naik 已提交
719
    # Allows Active Resource objects to be used as parameters in Action Pack URL generation.
720 721 722 723
    def to_param
      id && id.to_s
    end

724
    # Test for equality.  Resource are equal if and only if +other+ is the same object or
P
Pratik Naik 已提交
725
    # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+.
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
    #
    # ==== Examples
    #   ryan = Person.create(:name => 'Ryan')
    #   jamie = Person.create(:name => 'Jamie')
    #
    #   ryan == jamie
    #   # => false (Different name attribute and id)
    #
    #   ryan_again = Person.new(:name => 'Ryan')
    #   ryan == ryan_again
    #   # => false (ryan_again is new?)
    #
    #   ryans_clone = Person.create(:name => 'Ryan')
    #   ryan == ryans_clone
    #   # => false (Different id attributes)
    #
    #   ryans_twin = Person.find(ryan.id)
    #   ryan == ryans_twin
    #   # => true
    #
746 747 748 749
    def ==(other)
      other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
    end

750
    # Tests for equality (delegates to ==).
751 752 753 754
    def eql?(other)
      self == other
    end

P
Pratik Naik 已提交
755
    # Delegates to id in order to allow two resources of the same type and \id to work with something like:
756 757 758 759
    #   [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
    def hash
      id.hash
    end
760

761 762 763 764 765
    # Duplicate the current resource without saving it.
    #
    # ==== Examples
    #   my_invoice = Invoice.create(:customer => 'That Company')
    #   next_invoice = my_invoice.dup
P
Pratik Naik 已提交
766
    #   next_invoice.new? # => true
767 768
    #
    #   next_invoice.save
P
Pratik Naik 已提交
769
    #   next_invoice == my_invoice # => false (different id attributes)
770
    #
P
Pratik Naik 已提交
771 772
    #   my_invoice.customer   # => That Company
    #   next_invoice.customer # => That Company
773
    def dup
774
      returning self.class.new do |resource|
775 776 777 778
        resource.attributes     = @attributes
        resource.prefix_options = @prefix_options
      end
    end
779

P
Pratik Naik 已提交
780 781 782
    # A method to \save (+POST+) or \update (+PUT+) a resource.  It delegates to +create+ if a \new object, 
    # +update+ if it is existing. 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+
783 784 785 786
    # that weren't part of the original submit).
    #
    # ==== Examples
    #   my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
P
Pratik Naik 已提交
787 788
    #   my_company.new? # => true
    #   my_company.save # sends POST /companies/ (create)
789
    #
P
Pratik Naik 已提交
790
    #   my_company.new? # => false
791
    #   my_company.size = 10
P
Pratik Naik 已提交
792
    #   my_company.save # sends PUT /companies/1 (update)
793
    def save
794
      new? ? create : update
795 796
    end

797 798 799 800 801 802
    # Deletes the resource from the remote service.
    #
    # ==== Examples
    #   my_id = 3
    #   my_person = Person.find(my_id)
    #   my_person.destroy
P
Pratik Naik 已提交
803
    #   Person.find(my_id) # 404 (Resource Not Found)
804
    #
805
    #   new_person = Person.create(:name => 'James')
P
Pratik Naik 已提交
806
    #   new_id = new_person.id # => 7
807
    #   new_person.destroy
P
Pratik Naik 已提交
808
    #   Person.find(new_id) # 404 (Resource Not Found)
809
    def destroy
810
      connection.delete(element_path, self.class.headers)
811
    end
812

P
Pratik Naik 已提交
813
    # Evaluates to <tt>true</tt> if this resource is not <tt>new?</tt> and is
814 815 816 817 818 819 820
    # found on the remote service.  Using this method, you can check for
    # resources that may have been deleted between the object's instantiation
    # and actions on it.
    #
    # ==== Examples
    #   Person.create(:name => 'Theodore Roosevelt')
    #   that_guy = Person.find(:first)
P
Pratik Naik 已提交
821
    #   that_guy.exists? # => true
822 823
    #
    #   that_lady = Person.new(:name => 'Paul Bean')
P
Pratik Naik 已提交
824
    #   that_lady.exists? # => false
825 826 827
    #
    #   guys_id = that_guy.id
    #   Person.delete(guys_id)
P
Pratik Naik 已提交
828
    #   that_guy.exists? # => false
829
    def exists?
P
Pratik Naik 已提交
830
      !new? && self.class.exists?(to_param, :params => prefix_options)
831 832
    end

833 834 835 836 837
    # A method to convert the the resource to an XML string.
    #
    # ==== Options
    # The +options+ parameter is handed off to the +to_xml+ method on each
    # attribute, so it has the same options as the +to_xml+ methods in
P
Pratik Naik 已提交
838
    # Active Support.
839
    #
P
Pratik Naik 已提交
840 841 842
    # * <tt>:indent</tt> - Set the indent level for the XML output (default is +2+).
    # * <tt>:dasherize</tt> - Boolean option to determine whether or not element names should
    #   replace underscores with dashes (default is <tt>false</tt>).
843
    # * <tt>:skip_instruct</tt> - Toggle skipping the +instruct!+ call on the XML builder
P
Pratik Naik 已提交
844
    #   that generates the XML declaration (default is <tt>false</tt>).
845 846 847 848 849 850 851 852 853 854 855 856 857
    #
    # ==== Examples
    #   my_group = SubsidiaryGroup.find(:first)
    #   my_group.to_xml
    #   # => <?xml version="1.0" encoding="UTF-8"?>
    #   #    <subsidiary_group> [...] </subsidiary_group>
    #
    #   my_group.to_xml(:dasherize => true)
    #   # => <?xml version="1.0" encoding="UTF-8"?>
    #   #    <subsidiary-group> [...] </subsidiary-group>
    #
    #   my_group.to_xml(:skip_instruct => true)
    #   # => <subsidiary_group> [...] </subsidiary_group>
858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
    def to_xml(options={})
      attributes.to_xml({:root => self.class.element_name}.merge(options))
    end

    # Returns a JSON string representing the model. Some configuration is
    # available through +options+.
    #
    # ==== Options
    # The +options+ are passed to the +to_json+ method on each
    # attribute, so the same options as the +to_json+ methods in
    # Active Support.
    #
    # * <tt>:only</tt> - Only include the specified attribute or list of
    #   attributes in the serialized output. Attribute names must be specified
    #   as strings.
    # * <tt>:except</tt> - Do not include the specified attribute or list of
    #   attributes in the serialized output. Attribute names must be specified
    #   as strings.
    #
    # ==== Examples
    #   person = Person.new(:first_name => "Jim", :last_name => "Smith")
    #   person.to_json
    #   # => {"first_name": "Jim", "last_name": "Smith"}
    #
    #   person.to_json(:only => ["first_name"])
    #   # => {"first_name": "Jim"}
    #
    #   person.to_json(:except => ["first_name"])
    #   # => {"last_name": "Smith"}
    def to_json(options={})
      attributes.to_json(options)
    end

    # Returns the serialized string representation of the resource in the configured
    # serialization format specified in ActiveResource::Base.format. The options
    # applicable depend on the configured encoding format.
894 895 896 897 898 899 900
    def encode(options={})
      case self.class.format
        when ActiveResource::Formats[:xml]
          self.class.format.encode(attributes, {:root => self.class.element_name}.merge(options))
        else
          self.class.format.encode(attributes, options)
      end
901
    end
902

P
Pratik Naik 已提交
903
    # A method to \reload the attributes of this object from the remote web service.
904 905 906
    #
    # ==== Examples
    #   my_branch = Branch.find(:first)
P
Pratik Naik 已提交
907
    #   my_branch.name # => "Wislon Raod"
908
    #
909 910
    #   # Another client fixes the typo...
    #
P
Pratik Naik 已提交
911
    #   my_branch.name # => "Wislon Raod"
912
    #   my_branch.reload
P
Pratik Naik 已提交
913
    #   my_branch.name # => "Wilson Road"
914
    def reload
915
      self.load(self.class.find(to_param, :params => @prefix_options).attributes)
916 917
    end

P
Pratik Naik 已提交
918 919
    # A method to manually load attributes from a \hash. Recursively loads collections of
    # resources.  This method is called in +initialize+ and +create+ when a \hash of attributes
920 921 922 923
    # is provided.
    #
    # ==== Examples
    #   my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
924
    #   my_attrs = {:name => 'Marty', :colors => ["red", "green", "blue"]}
925 926
    #
    #   the_supplier = Supplier.find(:first)
P
Pratik Naik 已提交
927
    #   the_supplier.name # => 'J&M Textiles'
928 929 930 931 932 933 934 935 936 937 938
    #   the_supplier.load(my_attrs)
    #   the_supplier.name('J&J Textiles')
    #
    #   # These two calls are the same as Supplier.new(my_attrs)
    #   my_supplier = Supplier.new
    #   my_supplier.load(my_attrs)
    #
    #   # These three calls are the same as Supplier.create(my_attrs)
    #   your_supplier = Supplier.new
    #   your_supplier.load(my_attrs)
    #   your_supplier.save
939
    def load(attributes)
J
Jeremy Kemper 已提交
940
      raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
941
      @prefix_options, attributes = split_options(attributes)
942 943 944 945 946
      attributes.each do |key, value|
        @attributes[key.to_s] =
          case value
            when Array
              resource = find_or_create_resource_for_collection(key)
947
              value.map { |attrs| attrs.is_a?(String) ? attrs.dup : resource.new(attrs) }
948
            when Hash
949 950
              resource = find_or_create_resource_for(key)
              resource.new(value)
951 952 953 954
            else
              value.dup rescue value
          end
      end
955 956
      self
    end
957

P
Pratik Naik 已提交
958
    # For checking <tt>respond_to?</tt> without searching the attributes (which is faster).
959 960
    alias_method :respond_to_without_attributes?, :respond_to?

P
Pratik Naik 已提交
961
    # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a Person object with a
962 963
    # +name+ attribute can answer <tt>true</tt> to <tt>my_person.respond_to?(:name)</tt>, <tt>my_person.respond_to?(:name=)</tt>, and
    # <tt>my_person.respond_to?(:name?)</tt>.
964 965 966 967 968
    def respond_to?(method, include_priv = false)
      method_name = method.to_s
      if attributes.nil?
        return super
      elsif attributes.has_key?(method_name)
969
        return true
970 971 972 973 974 975 976
      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
977

978

979 980 981 982
    protected
      def connection(refresh = false)
        self.class.connection(refresh)
      end
983

984
      # Update the resource on the remote service.
985
      def update
986
        returning connection.put(element_path(prefix_options), encode, self.class.headers) do |response|
987 988
          load_attributes_from_response(response)
        end
989
      end
990

P
Pratik Naik 已提交
991
      # Create (i.e., \save to the remote service) the \new resource.
992
      def create
993
        returning connection.post(collection_path, encode, self.class.headers) do |response|
994
          self.id = id_from_response(response)
995
          load_attributes_from_response(response)
996
        end
997
      end
998

999
      def load_attributes_from_response(response)
1000
        if response['Content-Length'] != "0" && response.body.strip.size > 0
1001
          load(self.class.format.decode(response.body))
1002
        end
1003
      end
1004

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

1010
      def element_path(options = nil)
1011
        self.class.element_path(to_param, options || prefix_options)
1012 1013 1014 1015 1016 1017
      end

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

1018
    private
1019
      # Tries to find a resource for a given collection name; if it fails, then the resource is created
1020 1021 1022
      def find_or_create_resource_for_collection(name)
        find_or_create_resource_for(name.to_s.singularize)
      end
1023

1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
      # Tries to find a resource in a non empty list of nested modules
      # Raises a NameError if it was not found in any of the given nested modules
      def find_resource_in_modules(resource_name, module_names)
        receiver = Object
        namespaces = module_names[0, module_names.size-1].map do |module_name|
          receiver = receiver.const_get(module_name)
        end
        if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(resource_name) }
          return namespace.const_get(resource_name)
        else
          raise NameError
        end
      end

1038
      # Tries to find a resource for a given name; if it fails, then the resource is created
1039 1040
      def find_or_create_resource_for(name)
        resource_name = name.to_s.camelize
1041 1042 1043
        ancestors = self.class.name.split("::")
        if ancestors.size > 1
          find_resource_in_modules(resource_name, ancestors)
1044 1045 1046
        else
          self.class.const_get(resource_name)
        end
1047
      rescue NameError
1048 1049 1050 1051 1052
        if self.class.const_defined?(resource_name)
          resource = self.class.const_get(resource_name)
        else
          resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
        end
1053
        resource.prefix = self.class.prefix
1054
        resource.site   = self.class.site
1055 1056 1057
        resource
      end

1058
      def split_options(options = {})
1059
        self.class.__send__(:split_options, options)
1060 1061
      end

1062
      def method_missing(method_symbol, *arguments) #:nodoc:
1063
        method_name = method_symbol.to_s
1064

1065 1066 1067 1068
        case method_name.last
          when "="
            attributes[method_name.first(-1)] = arguments.first
          when "?"
T
 
Tobias Lütke 已提交
1069
            attributes[method_name.first(-1)]
1070
          else
1071
            attributes.has_key?(method_name) ? attributes[method_name] : super
1072 1073 1074
        end
      end
  end
J
Jeremy Kemper 已提交
1075
end