Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activeresource/lib/active_resource/base.rb

Revision 9145, 36.5 kB (checked in by bitsweat, 2 years ago)

Don't shadow attrs var

Line 
1 require 'active_resource/connection'
2 require 'cgi'
3 require 'set'
4
5 module ActiveResource
6   # ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
7   #
8   # For an outline of what Active Resource is capable of, see link:files/vendor/rails/activeresource/README.html.
9   #
10   # == Automated mapping
11   #
12   # Active Resource objects represent your RESTful resources as manipulatable Ruby objects.  To map resources
13   # to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
14   # Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
15   # URI of the resources.
16   #
17   #     class Person < ActiveResource::Base
18   #       self.site = "http://api.people.com:3000/"
19   #     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. 
23   #
24   # == Lifecycle methods
25   #
26   # Active Resource exposes methods for creating, finding, updating, and deleting resources
27   # from REST web services.
28   #
29   #   ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
30   #   ryan.save  #=> true
31   #   ryan.id  #=> 2
32   #   Person.exists?(ryan.id)  #=> true
33   #   ryan.exists?  #=> true
34   #
35   #   ryan = Person.find(1)
36   #   # => Resource holding our newly created Person object
37   #
38   #   ryan.first = 'Rizzle'
39   #   ryan.save  #=> true
40   #
41   #   ryan.destroy  #=> true
42   #
43   # As you can see, these are very similar to Active Record's lifecycle methods for database records.
44   # You can read more about each of these methods in their respective documentation.
45   #
46   # === Custom REST methods
47   #
48   # Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
49   # defining your own custom REST methods. To invoke them, Active Resource provides the <tt>get</tt>,
50   # <tt>post</tt>, <tt>put</tt> and <tt>delete</tt> methods where you can specify a custom REST method
51   # name to invoke.
52   #
53   #   # POST to the custom 'register' REST method, i.e. POST /people/new/register.xml.
54   #   Person.new(:name => 'Ryan').post(:register)
55   #   # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
56   #
57   #   # PUT an update by invoking the 'promote' REST method, i.e. PUT /people/1/promote.xml?position=Manager.
58   #   Person.find(1).put(:promote, :position => 'Manager')
59   #   # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
60   #
61   #   # GET all the positions available, i.e. GET /people/positions.xml.
62   #   Person.get(:positions)
63   #   # => [{:name => 'Manager'}, {:name => 'Clerk'}]
64   #
65   #   # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.xml.
66   #   Person.find(1).delete(:fire)
67   #
68   # For more information on using custom REST methods, see the
69   # ActiveResource::CustomMethods documentation.
70   #
71   # == Validations
72   #
73   # You can validate resources client side by overriding validation methods in the base class.
74   #
75   #     class Person < ActiveResource::Base
76   #        self.site = "http://api.people.com:3000/"
77   #        protected
78   #          def validate
79   #            errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
80   #          end
81   #     end
82   #
83   # See the ActiveResource::Validations documentation for more information.
84   #
85   # == Authentication
86   #
87   # Many REST APIs will require authentication, usually in the form of basic
88   # HTTP authentication.  Authentication can be specified by:
89   # * putting the credentials in the URL for the +site+ variable.
90   #
91   #    class Person < ActiveResource::Base
92   #      self.site = "http://ryan:password@api.people.com:3000/"
93   #    end
94   #
95   # * defining +user+ and/or +password+ variables
96   #
97   #    class Person < ActiveResource::Base
98   #      self.site = "http://api.people.com:3000/"
99   #      self.user = "ryan"
100   #      self.password = "password"
101   #    end
102   #
103   # For obvious security reasons, it is probably best if such services are available
104   # over HTTPS.
105   #
106   # Note: Some values cannot be provided in the URL passed to site.  e.g. email addresses
107   # as usernames.  In those situations you should use the seperate user and password option.
108   # == Errors & Validation
109   #
110   # Error handling and validation is handled in much the same manner as you're used to seeing in
111   # Active Record.  Both the response code in the HTTP response and the body of the response are used to
112   # indicate that an error occurred.
113   #
114   # === Resource errors
115   #
116   # When a GET is requested for a resource that does not exist, the HTTP <tt>404</tt> (Resource Not Found)
117   # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
118   # exception.
119   #
120   #   # GET http://api.people.com:3000/people/999.xml
121   #   ryan = Person.find(999) # => Raises ActiveResource::ResourceNotFound
122   #   # => Response = 404
123   #
124   # <tt>404</tt> is just one of the HTTP error response codes that ActiveResource will handle with its own exception. The
125   # following HTTP response codes will also result in these exceptions:
126   #
127   # 200 - 399:: Valid response, no exception
128   # 404:: ActiveResource::ResourceNotFound
129   # 409:: ActiveResource::ResourceConflict
130   # 422:: ActiveResource::ResourceInvalid (rescued by save as validation errors)
131   # 401 - 499:: ActiveResource::ClientError
132   # 500 - 599:: ActiveResource::ServerError
133   #
134   # These custom exceptions allow you to deal with resource errors more naturally and with more precision
135   # rather than returning a general HTTP error.  For example:
136   #
137   #   begin
138   #     ryan = Person.find(my_id)
139   #   rescue ActiveResource::ResourceNotFound
140   #     redirect_to :action => 'not_found'
141   #   rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
142   #     redirect_to :action => 'new'
143   #   end
144   #
145   # === Validation errors
146   #
147   # Active Resource supports validations on resources and will return errors if any these validations fail
148   # (e.g., "First name can not be blank" and so on).  These types of errors are denoted in the response by
149   # a response code of <tt>422</tt> and an XML representation of the validation errors.  The save operation will
150   # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
151   #
152   #   ryan = Person.find(1)
153   #   ryan.first #=> ''
154   #   ryan.save  #=> false
155   #
156   #   # When
157   #   # PUT http://api.people.com:3000/people/1.xml
158   #   # is requested with invalid values, the response is:
159   #   #
160   #   # Response (422):
161   #   # <errors type="array"><error>First cannot be empty</error></errors>
162   #   #
163   #
164   #   ryan.errors.invalid?(:first)  #=> true
165   #   ryan.errors.full_messages  #=> ['First cannot be empty']
166   #
167   # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
168   #
169   class Base
170     # The logger for diagnosing and tracing Active Resource calls.
171     cattr_accessor :logger
172
173     class << self
174       # Gets the URI of the REST resources to map for this class.  The site variable is required
175       # ActiveResource's mapping to work.
176       def site
177         # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
178         #
179         # With superclass_delegating_reader
180         #
181         #   Parent.site = 'http://anonymous@test.com'
182         #   Subclass.site # => 'http://anonymous@test.com'
183         #   Subclass.site.user = 'david'
184         #   Parent.site # => 'http://david@test.com'
185         #
186         # Without superclass_delegating_reader (expected behaviour)
187         #
188         #   Parent.site = 'http://anonymous@test.com'
189         #   Subclass.site # => 'http://anonymous@test.com'
190         #   Subclass.site.user = 'david' # => TypeError: can't modify frozen object
191         #
192         if defined?(@site)
193           @site
194         elsif superclass != Object && superclass.site
195           superclass.site.dup.freeze
196         end
197       end
198
199       # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
200       # The site variable is required ActiveResource's mapping to work.
201       def site=(site)
202         @connection = nil
203         if site.nil?
204           @site = nil
205         else
206           @site = create_site_uri_from(site)
207           @user = URI.decode(@site.user) if @site.user
208           @password = URI.decode(@site.password) if @site.password
209         end
210       end
211
212       # Gets the user for REST HTTP authentication
213       def user
214         # Not using superclass_delegating_reader. See +site+ for explanation
215         if defined?(@user)
216           @user
217         elsif superclass != Object && superclass.user
218           superclass.user.dup.freeze
219         end
220       end
221
222       # Sets the user for REST HTTP authentication
223       def user=(user)
224         @connection = nil
225         @user = user
226       end
227
228       # Gets the password for REST HTTP authentication
229       def password
230         # Not using superclass_delegating_reader. See +site+ for explanation
231         if defined?(@password)
232           @password
233         elsif superclass != Object && superclass.password
234           superclass.password.dup.freeze
235         end
236       end
237
238       # Sets the password for REST HTTP authentication
239       def password=(password)
240         @connection = nil
241         @password = password
242       end
243
244       # Sets the format that attributes are sent and received in from a mime type reference. Example:
245       #
246       #   Person.format = :json
247       #   Person.find(1) # => GET /people/1.json
248       #
249       #   Person.format = ActiveResource::Formats::XmlFormat
250       #   Person.find(1) # => GET /people/1.xml
251       #
252       # Default format is :xml.
253       def format=(mime_type_reference_or_format)
254         format = mime_type_reference_or_format.is_a?(Symbol) ?
255           ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
256
257         write_inheritable_attribute("format", format)
258         connection.format = format if site
259       end
260      
261       # Returns the current format, default is ActiveResource::Formats::XmlFormat
262       def format # :nodoc:
263         read_inheritable_attribute("format") || ActiveResource::Formats[:xml]
264       end
265
266       # An instance of ActiveResource::Connection that is the base connection to the remote service.
267       # The +refresh+ parameter toggles whether or not the connection is refreshed at every request
268       # or not (defaults to <tt>false</tt>).
269       def connection(refresh = false)
270         if defined?(@connection) || superclass == Object
271           @connection = Connection.new(site, format) if refresh || @connection.nil?
272           @connection.user = user if user
273           @connection.password = password if password
274           @connection
275         else
276           superclass.connection
277         end
278       end
279
280       def headers
281         @headers ||= {}
282       end
283
284       # Do not include any modules in the default element name. This makes it easier to seclude ARes objects
285       # in a separate namespace without having to set element_name repeatedly.
286       attr_accessor_with_default(:element_name)    { to_s.split("::").last.underscore } #:nodoc:
287
288       attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
289       attr_accessor_with_default(:primary_key, 'id') #:nodoc:
290      
291       # Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
292       # This method is regenerated at runtime based on what the prefix is set to.
293       def prefix(options={})
294         default = site.path
295         default << '/' unless default[-1..-1] == '/'
296         # generate the actual method based on the current site path
297         self.prefix = default
298         prefix(options)
299       end
300
301       # An attribute reader for the source string for the resource path prefix.  This
302       # method is regenerated at runtime based on what the prefix is set to.
303       def prefix_source
304         prefix # generate #prefix and #prefix_source methods first
305         prefix_source
306       end
307
308       # Sets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
309       # Default value is <tt>site.path</tt>.
310       def prefix=(value = '/')
311         # Replace :placeholders with '#{embedded options[:lookups]}'
312         prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
313
314         # Redefine the new methods.
315         code = <<-end_code
316           def prefix_source() "#{value}" end
317           def prefix(options={}) "#{prefix_call}" end
318         end_code
319         silence_warnings { instance_eval code, __FILE__, __LINE__ }
320       rescue
321         logger.error "Couldn't set prefix: #{$!}\n  #{code}"
322         raise
323       end
324
325       alias_method :set_prefix, :prefix=  #:nodoc:
326
327       alias_method :set_element_name, :element_name=  #:nodoc:
328       alias_method :set_collection_name, :collection_name=  #:nodoc:
329
330       # Gets the element path for the given ID in +id+.  If the +query_options+ parameter is omitted, Rails
331       # will split from the prefix options.
332       #
333       # ==== Options
334       # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
335       #                    would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
336       # +query_options+:: A hash to add items to the query string for the request.
337       #
338       # ==== Examples
339       #   Post.element_path(1)
340       #   # => /posts/1.xml
341       #
342       #   Comment.element_path(1, :post_id => 5)
343       #   # => /posts/5/comments/1.xml
344       #
345       #   Comment.element_path(1, :post_id => 5, :active => 1)
346       #   # => /posts/5/comments/1.xml?active=1
347       #
348       #   Comment.element_path(1, {:post_id => 5}, {:active => 1})
349       #   # => /posts/5/comments/1.xml?active=1
350       #
351       def element_path(id, prefix_options = {}, query_options = nil)
352         prefix_options, query_options = split_options(prefix_options) if query_options.nil?
353         "#{prefix(prefix_options)}#{collection_name}/#{id}.#{format.extension}#{query_string(query_options)}"       
354       end
355
356       # Gets the collection path for the REST resources.  If the +query_options+ parameter is omitted, Rails
357       # will split from the +prefix_options+.
358       #
359       # ==== Options
360       # +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
361       #                    would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
362       # +query_options+:: A hash to add items to the query string for the request.
363       #
364       # ==== Examples
365       #   Post.collection_path
366       #   # => /posts.xml
367       #
368       #   Comment.collection_path(:post_id => 5)
369       #   # => /posts/5/comments.xml
370       #
371       #   Comment.collection_path(:post_id => 5, :active => 1)
372       #   # => /posts/5/comments.xml?active=1
373       #
374       #   Comment.collection_path({:post_id => 5}, {:active => 1})
375       #   # => /posts/5/comments.xml?active=1
376       #
377       def collection_path(prefix_options = {}, query_options = nil)
378         prefix_options, query_options = split_options(prefix_options) if query_options.nil?
379         "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
380       end
381
382       alias_method :set_primary_key, :primary_key=  #:nodoc:
383
384       # Create a new resource instance and request to the remote service
385       # that it be saved, making it equivalent to the following simultaneous calls:
386       #
387       #   ryan = Person.new(:first => 'ryan')
388       #   ryan.save
389       #
390       # The newly created resource is returned.  If a failure has occurred an
391       # exception will be raised (see save).  If the resource is invalid and
392       # has not been saved then valid? will return <tt>false</tt>,
393       # while new? will still return <tt>true</tt>.
394       #
395       # ==== Examples
396       #   Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
397       #   my_person = Person.find(:first)
398       #   my_person.email
399       #   # => myname@nospam.com
400       #
401       #   dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
402       #   dhh.valid?
403       #   # => true
404       #   dhh.new?
405       #   # => false
406       #
407       #   # We'll assume that there's a validation that requires the name attribute
408       #   that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
409       #   that_guy.valid?
410       #   # => false
411       #   that_guy.new?
412       #   # => true
413       #
414       def create(attributes = {})
415         returning(self.new(attributes)) { |res| res.save }       
416       end
417
418       # Core method for finding resources.  Used similarly to Active Record's find method.
419       #
420       # ==== Arguments
421       # The first argument is considered to be the scope of the query.  That is, how many
422       # resources are returned from the request.  It can be one of the following.
423       #
424       # +:one+:: Returns a single resource.
425       # +:first+:: Returns the first resource found.
426       # +:all+:: Returns every resource that matches the request.
427       #
428       # ==== Options
429       # +from+:: Sets the path or custom method that resources will be fetched from.
430       # +params+:: Sets query and prefix (nested URL) parameters.
431       #
432       # ==== Examples
433       #   Person.find(1)                                         
434       #   # => GET /people/1.xml
435       #
436       #   Person.find(:all)                                     
437       #   # => GET /people.xml
438       #
439       #   Person.find(:all, :params => { :title => "CEO" })     
440       #   # => GET /people.xml?title=CEO
441       #
442       #   Person.find(:first, :from => :managers)                 
443       #   # => GET /people/managers.xml
444       #
445       #   Person.find(:all, :from => "/companies/1/people.xml") 
446       #   # => GET /companies/1/people.xml
447       #
448       #   Person.find(:one, :from => :leader)                   
449       #   # => GET /people/leader.xml
450       #
451       #   Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
452       #   # => GET /people/developers.xml?language=ruby
453       #
454       #   Person.find(:one, :from => "/companies/1/manager.xml")
455       #   # => GET /companies/1/manager.xml
456       #
457       #   StreetAddress.find(1, :params => { :person_id => 1 }) 
458       #   # => GET /people/1/street_addresses/1.xml
459       def find(*arguments)
460         scope   = arguments.slice!(0)
461         options = arguments.slice!(0) || {}
462
463         case scope
464           when :all   then find_every(options)
465           when :first then find_every(options).first
466           when :one   then find_one(options)
467           else             find_single(scope, options)
468         end
469       end
470
471       # Deletes the resources with the ID in the +id+ parameter.
472       #
473       # ==== Options
474       # All options specify prefix and query parameters.
475       #
476       # ==== Examples
477       #   Event.delete(2)
478       #   # => DELETE /events/2
479       #
480       #   Event.create(:name => 'Free Concert', :location => 'Community Center')
481       #   my_event = Event.find(:first)
482       #   # => Events (id: 7)
483       #   Event.delete(my_event.id)
484       #   # => DELETE /events/7
485       #
486       #   # Let's assume a request to events/5/cancel.xml
487       #   Event.delete(params[:id])
488       #   # => DELETE /events/5
489       #
490       def delete(id, options = {})
491         connection.delete(element_path(id, options))
492       end
493
494       # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
495       #
496       # ==== Examples
497       #   Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
498       #   Note.exists?(1)
499       #   # => true
500       #
501       #   Note.exists(1349)
502       #   # => false
503       def exists?(id, options = {})
504         if id
505           prefix_options, query_options = split_options(options[:params])
506           path = element_path(id, prefix_options, query_options)
507           response = connection.head(path, headers)
508           response.code == 200
509         end
510         # id && !find_single(id, options).nil?
511       rescue ActiveResource::ResourceNotFound
512         false
513       end
514
515       private
516         # Find every resource
517         def find_every(options)
518           case from = options[:from]
519           when Symbol
520             instantiate_collection(get(from, options[:params]))
521           when String
522             path = "#{from}#{query_string(options[:params])}"
523             instantiate_collection(connection.get(path, headers) || [])
524           else
525             prefix_options, query_options = split_options(options[:params])
526             path = collection_path(prefix_options, query_options)
527             instantiate_collection( (connection.get(path, headers) || []), prefix_options )
528           end
529         end
530        
531         # Find a single resource from a one-off URL
532         def find_one(options)
533           case from = options[:from]
534           when Symbol
535             instantiate_record(get(from, options[:params]))
536           when String
537             path = "#{from}#{query_string(options[:params])}"
538             instantiate_record(connection.get(path, headers))
539           end
540         end
541
542         # Find a single resource from the default URL
543         def find_single(scope, options)
544           prefix_options, query_options = split_options(options[:params])
545           path = element_path(scope, prefix_options, query_options)
546           instantiate_record(connection.get(path, headers), prefix_options)
547         end
548        
549         def instantiate_collection(collection, prefix_options = {})
550           collection.collect! { |record| instantiate_record(record, prefix_options) }
551         end
552
553         def instantiate_record(record, prefix_options = {})
554           returning new(record) do |resource|
555             resource.prefix_options = prefix_options
556           end
557         end
558
559
560         # Accepts a URI and creates the site URI from that.
561         def create_site_uri_from(site)
562           site.is_a?(URI) ? site.dup : URI.parse(site)
563         end
564
565         # contains a set of the current prefix parameters.
566         def prefix_parameters
567           @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
568         end
569
570         # Builds the query string for the request.
571         def query_string(options)
572           "?#{options.to_query}" unless options.nil? || options.empty?
573         end
574
575         # split an option hash into two hashes, one containing the prefix options,
576         # and the other containing the leftovers.
577         def split_options(options = {})
578           prefix_options, query_options = {}, {}
579
580           (options || {}).each do |key, value|
581             next if key.blank?
582             (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
583           end
584
585           [ prefix_options, query_options ]
586         end
587     end
588
589     attr_accessor :attributes #:nodoc:
590     attr_accessor :prefix_options #:nodoc:
591
592     # Constructor method for new resources; the optional +attributes+ parameter takes a +Hash+
593     # of attributes for the new resource.
594     #
595     # ==== Examples
596     #   my_course = Course.new
597     #   my_course.name = "Western Civilization"
598     #   my_course.lecturer = "Don Trotter"
599     #   my_course.save
600     #
601     #   my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
602     #   my_other_course.save
603     def initialize(attributes = {})
604       @attributes     = {}
605       @prefix_options = {}
606       load(attributes)
607     end
608
609     # Returns a clone of the resource that hasn't been assigned an id yet and
610     # is treated as a new resource.
611     #
612     #  ryan = Person.find(1)
613     #  not_ryan = ryan.clone
614     #  not_ryan.new?  # => true
615     #
616     # Any active resource member attributes will NOT be cloned, though all other
617     # attributes are.  This is to prevent the conflict between any prefix_options
618     # that refer to the original parent resource and the newly cloned parent
619     # resource that does not exist.
620     #
621     #  ryan = Person.find(1)
622     #  ryan.address = StreetAddress.find(1, :person_id => ryan.id)
623     #  ryan.hash = {:not => "an ARes instance"}
624     #
625     #  not_ryan = ryan.clone
626     #  not_ryan.new?            # => true
627     #  not_ryan.address         # => NoMethodError
628     #  not_ryan.hash            # => {:not => "an ARes instance"}
629     #
630     def clone
631       # Clone all attributes except the pk and any nested ARes
632       cloned = attributes.reject {|k,v| k == self.class.primary_key || v.is_a?(ActiveResource::Base)}.inject({}) do |attrs, (k, v)|
633         attrs[k] = v.clone
634         attrs
635       end
636       # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
637       # attempts to convert hashes into member objects and arrays into collections of objects.  We want
638       # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
639       resource = self.class.new({})
640       resource.prefix_options = self.prefix_options
641       resource.send :instance_variable_set, '@attributes', cloned
642       resource
643     end
644
645
646     # A method to determine if the resource a new object (i.e., it has not been POSTed to the remote service yet).
647     #
648     # ==== Examples
649     #   not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
650     #   not_new.new?
651     #   # => false
652     #
653     #   is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
654     #   is_new.new?
655     #   # => true
656     #
657     #   is_new.save
658     #   is_new.new?
659     #   # => false
660     #
661     def new?
662       id.nil?
663     end
664
665     # Get the +id+ attribute of the resource.
666     def id
667       attributes[self.class.primary_key]
668     end
669
670     # Set the +id+ attribute of the resource.
671     def id=(id)
672       attributes[self.class.primary_key] = id
673     end
674
675     # Allows ActiveResource objects to be used as parameters in ActionPack URL generation.
676     def to_param
677       id && id.to_s
678     end
679
680     # Test for equality.  Resource are equal if and only if +other+ is the same object or
681     # is an instance of the same class, is not +new?+, and has the same +id+.
682     #
683     # ==== Examples
684     #   ryan = Person.create(:name => 'Ryan')
685     #   jamie = Person.create(:name => 'Jamie')
686     #
687     #   ryan == jamie
688     #   # => false (Different name attribute and id)
689     #
690     #   ryan_again = Person.new(:name => 'Ryan')
691     #   ryan == ryan_again
692     #   # => false (ryan_again is new?)
693     #
694     #   ryans_clone = Person.create(:name => 'Ryan')
695     #   ryan == ryans_clone
696     #   # => false (Different id attributes)
697     #
698     #   ryans_twin = Person.find(ryan.id)
699     #   ryan == ryans_twin
700     #   # => true
701     #
702     def ==(other)
703       other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
704     end
705
706     # Tests for equality (delegates to ==).
707     def eql?(other)
708       self == other
709     end
710
711     # Delegates to id in order to allow two resources of the same type and id to work with something like:
712     #   [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
713     def hash
714       id.hash
715     end
716    
717     # Duplicate the current resource without saving it.
718     #
719     # ==== Examples
720     #   my_invoice = Invoice.create(:customer => 'That Company')
721     #   next_invoice = my_invoice.dup
722     #   next_invoice.new?
723     #   # => true
724     #
725     #   next_invoice.save
726     #   next_invoice == my_invoice
727     #   # => false (different id attributes)
728     #
729     #   my_invoice.customer
730     #   # => That Company
731     #   next_invoice.customer
732     #   # => That Company
733     def dup
734       returning self.class.new do |resource|
735         resource.attributes     = @attributes
736         resource.prefix_options = @prefix_options
737       end
738     end
739
740     # A method to save (+POST+) or update (+PUT+) a resource.  It delegates to +create+ if a new object,
741     # +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body
742     # is XML for the final object as it looked after the save (which would include attributes like +created_at+
743     # that weren't part of the original submit).
744     #
745     # ==== Examples
746     #   my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
747     #   my_company.new?
748     #   # => true
749     #   my_company.save
750     #   # => POST /companies/ (create)
751     #
752     #   my_company.new?
753     #   # => false
754     #   my_company.size = 10
755     #   my_company.save
756     #   # => PUT /companies/1 (update)
757     def save
758       new? ? create : update
759     end
760
761     # Deletes the resource from the remote service.
762     #
763     # ==== Examples
764     #   my_id = 3
765     #   my_person = Person.find(my_id)
766     #   my_person.destroy
767     #   Person.find(my_id)
768     #   # => 404 (Resource Not Found)
769     #   
770     #   new_person = Person.create(:name => 'James')
771     #   new_id = new_person.id
772     #   # => 7
773     #   new_person.destroy
774     #   Person.find(new_id)
775     #   # => 404 (Resource Not Found)
776     def destroy
777       connection.delete(element_path, self.class.headers)
778     end
779
780     # Evaluates to <tt>true</tt> if this resource is not +new?+ and is
781     # found on the remote service.  Using this method, you can check for
782     # resources that may have been deleted between the object's instantiation
783     # and actions on it.
784     #
785     # ==== Examples
786     #   Person.create(:name => 'Theodore Roosevelt')
787     #   that_guy = Person.find(:first)
788     #   that_guy.exists?
789     #   # => true
790     #
791     #   that_lady = Person.new(:name => 'Paul Bean')
792     #   that_lady.exists?
793     #   # => false
794     #
795     #   guys_id = that_guy.id
796     #   Person.delete(guys_id)
797     #   that_guy.exists?
798     #   # => false
799     def exists?
800       !new? && self.class.exists?(to_param, :params => prefix_options)     
801     end
802
803     # A method to convert the the resource to an XML string.
804     #
805     # ==== Options
806     # The +options+ parameter is handed off to the +to_xml+ method on each
807     # attribute, so it has the same options as the +to_xml+ methods in
808     # ActiveSupport.
809     #
810     # indent:: Set the indent level for the XML output (default is +2+).
811     # dasherize:: Boolean option to determine whether or not element names should
812     #             replace underscores with dashes (default is <tt>false</tt>).
813     # skip_instruct::  Toggle skipping the +instruct!+ call on the XML builder
814     #                  that generates the XML declaration (default is <tt>false</tt>).
815     #
816     # ==== Examples
817     #   my_group = SubsidiaryGroup.find(:first)
818     #   my_group.to_xml
819     #   # => <?xml version="1.0" encoding="UTF-8"?>
820     #   #    <subsidiary_group> [...] </subsidiary_group>
821     #
822     #   my_group.to_xml(:dasherize => true)
823     #   # => <?xml version="1.0" encoding="UTF-8"?>
824     #   #    <subsidiary-group> [...] </subsidiary-group>
825     #
826     #   my_group.to_xml(:skip_instruct => true)
827     #   # => <subsidiary_group> [...] </subsidiary_group>
828     def to_xml(options={})
829       attributes.to_xml({:root => self.class.element_name}.merge(options))
830     end
831
832     # A method to reload the attributes of this object from the remote web service.
833     #
834     # ==== Examples
835     #   my_branch = Branch.find(:first)
836     #   my_branch.name
837     #   # => Wislon Raod
838     #   
839     #   # Another client fixes the typo...
840     #
841     #   my_branch.name
842     #   # => Wislon Raod
843     #   my_branch.reload
844     #   my_branch.name
845     #   # => Wilson Road
846     def reload
847       self.load(self.class.find(to_param, :params => @prefix_options).attributes)
848     end
849
850     # A method to manually load attributes from a hash. Recursively loads collections of
851     # resources.  This method is called in initialize and create when a +Hash+ of attributes
852     # is provided.
853     #
854     # ==== Examples
855     #   my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
856     #
857     #   the_supplier = Supplier.find(:first)
858     #   the_supplier.name
859     #   # => 'J&M Textiles'
860     #   the_supplier.load(my_attrs)
861     #   the_supplier.name('J&J Textiles')
862     #
863     #   # These two calls are the same as Supplier.new(my_attrs)
864     #   my_supplier = Supplier.new
865     #   my_supplier.load(my_attrs)
866     #
867     #   # These three calls are the same as Supplier.create(my_attrs)
868     #   your_supplier = Supplier.new
869     #   your_supplier.load(my_attrs)
870     #   your_supplier.save
871     def load(attributes)
872       raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
873       @prefix_options, attributes = split_options(attributes)
874       attributes.each do |key, value|
875         @attributes[key.to_s] =
876           case value
877             when Array
878               resource = find_or_create_resource_for_collection(key)
879               value.map { |attrs| resource.new(attrs) }
880             when Hash
881               resource = find_or_create_resource_for(key)
882               resource.new(value)
883             else
884               value.dup rescue value
885           end
886       end
887       self
888     end
889    
890     # For checking respond_to? without searching the attributes (which is faster).
891     alias_method :respond_to_without_attributes?, :respond_to?
892
893     # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a +Person+ object with a
894     # +name+ attribute can answer <tt>true</tt> to <tt>my_person.respond_to?("name")</tt>, <tt>my_person.respond_to?("name=")</tt>, and
895     # <tt>my_person.respond_to?("name?")</tt>.
896     def respond_to?(method, include_priv = false)
897       method_name = method.to_s
898       if attributes.nil?
899         return super
900       elsif attributes.has_key?(method_name)
901         return true
902       elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
903         return true
904       end
905       # super must be called at the end of the method, because the inherited respond_to?
906       # would return true for generated readers, even if the attribute wasn't present
907       super
908     end
909    
910
911     protected
912       def connection(refresh = false)
913         self.class.connection(refresh)
914       end
915
916       # Update the resource on the remote service.
917       def update
918         returning connection.put(element_path(prefix_options), to_xml, self.class.headers) do |response|
919           load_attributes_from_response(response)
920         end
921       end
922
923       # Create (i.e., save to the remote service) the new resource.
924       def create
925         returning connection.post(collection_path, to_xml, self.class.headers) do |response|
926           self.id = id_from_response(response)
927           load_attributes_from_response(response)
928         end
929       end
930      
931       def load_attributes_from_response(response)
932         if response['Content-Length'] != "0" && response.body.strip.size > 0
933           load(self.class.format.decode(response.body))
934         end
935       end
936
937       # Takes a response from a typical create post and pulls the ID out
938       def id_from_response(response)
939         response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
940       end
941
942       def element_path(options = nil)
943         self.class.element_path(to_param, options || prefix_options)
944       end
945
946       def collection_path(options = nil)
947         self.class.collection_path(options || prefix_options)
948       end
949
950     private
951       # Tries to find a resource for a given collection name; if it fails, then the resource is created
952       def find_or_create_resource_for_collection(name)
953         find_or_create_resource_for(name.to_s.singularize)
954       end
955      
956       # Tries to find a resource in a non empty list of nested modules
957       # Raises a NameError if it was not found in any of the given nested modules
958       def find_resource_in_modules(resource_name, module_names)
959         receiver = Object
960         namespaces = module_names[0, module_names.size-1].map do |module_name|
961           receiver = receiver.const_get(module_name)
962         end
963         if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(resource_name) }
964           return namespace.const_get(resource_name)
965         else
966           raise NameError
967         end
968       end
969
970       # Tries to find a resource for a given name; if it fails, then the resource is created
971       def find_or_create_resource_for(name)
972         resource_name = name.to_s.camelize
973         ancestors = self.class.name.split("::")
974         if ancestors.size > 1
975           find_resource_in_modules(resource_name, ancestors)
976         else
977           self.class.const_get(resource_name)
978         end
979       rescue NameError
980         resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
981         resource.prefix = self.class.prefix
982         resource.site   = self.class.site
983         resource
984       end
985
986       def split_options(options = {})
987         self.class.send!(:split_options, options)
988       end
989
990       def method_missing(method_symbol, *arguments) #:nodoc:
991         method_name = method_symbol.to_s
992
993         case method_name.last
994           when "="
995             attributes[method_name.first(-1)] = arguments.first
996           when "?"
997             attributes[method_name.first(-1)]
998           else
999             attributes.has_key?(method_name) ? attributes[method_name] : super
1000         end
1001       end
1002   end
1003 end
Note: See TracBrowser for help on using the browser.