1 require 'active_resource/connection'
6 # 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.
10 # == Automated mapping
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.
17 # class Person < ActiveResource::Base
18 # self.site = "http://api.people.com:3000/"
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
23 # an existing model with the same name as the desired RESTful resource you can set the +element_name+ value.
25 # class PersonResource < ActiveResource::Base
26 # self.site = "http://api.people.com:3000/"
27 # self.element_name = "person"
31 # == Lifecycle methods
33 # Active Resource exposes methods for creating, finding, updating, and deleting resources
34 # from REST web services.
36 # ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
39 # Person.exists?(ryan.id) # => true
40 # ryan.exists? # => true
42 # ryan = Person.find(1)
43 # # Resource holding our newly created Person object
45 # ryan.first = 'Rizzle'
48 # ryan.destroy # => true
50 # As you can see, these are very similar to Active Record's lifecycle methods for database records.
51 # You can read more about each of these methods in their respective documentation.
53 # === Custom REST methods
55 # 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>,
57 # <tt>post</tt>, <tt>put</tt> and <tt>\delete</tt> methods where you can specify a custom REST method
60 # # POST to the custom 'register' REST method, i.e. POST /people/new/register.xml.
61 # Person.new(:name => 'Ryan').post(:register)
62 # # => { :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' }
68 # # GET all the positions available, i.e. GET /people/positions.xml.
69 # Person.get(:positions)
70 # # => [{:name => 'Manager'}, {:name => 'Clerk'}]
72 # # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.xml.
73 # Person.find(1).delete(:fire)
75 # For more information on using custom REST methods, see the
76 # ActiveResource::CustomMethods documentation.
80 # You can validate resources client side by overriding validation methods in the base class.
82 # class Person < ActiveResource::Base
83 # self.site = "http://api.people.com:3000/"
86 # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
90 # See the ActiveResource::Validations documentation for more information.
94 # Many REST APIs will require authentication, usually in the form of basic
95 # HTTP authentication. Authentication can be specified by:
96 # * putting the credentials in the URL for the +site+ variable.
98 # class Person < ActiveResource::Base
99 # self.site = "http://ryan:password@api.people.com:3000/"
102 # * defining +user+ and/or +password+ variables
104 # class Person < ActiveResource::Base
105 # self.site = "http://api.people.com:3000/"
107 # self.password = "password"
110 # For obvious security reasons, it is probably best if such services are available
113 # Note: Some values cannot be provided in the URL passed to site. e.g. email addresses
114 # as usernames. In those situations you should use the separate user and password option.
115 # == Errors & Validation
117 # 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.
121 # === Resource errors
123 # When a GET is requested for a resource that does not exist, the HTTP <tt>404</tt> (Resource Not Found)
124 # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
127 # # GET http://api.people.com:3000/people/999.xml
128 # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
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:
133 # * 200..399 - Valid response, no exception (other than 301, 302)
134 # * 301, 302 - ActiveResource::Redirection
135 # * 400 - ActiveResource::BadRequest
136 # * 401 - ActiveResource::UnauthorizedAccess
137 # * 403 - ActiveResource::ForbiddenAccess
138 # * 404 - ActiveResource::ResourceNotFound
139 # * 405 - ActiveResource::MethodNotAllowed
140 # * 409 - ActiveResource::ResourceConflict
141 # * 422 - ActiveResource::ResourceInvalid (rescued by save as validation errors)
142 # * 401..499 - ActiveResource::ClientError
143 # * 500..599 - ActiveResource::ServerError
144 # * Other - ActiveResource::ConnectionError
146 # These custom exceptions allow you to deal with resource errors more naturally and with more precision
147 # rather than returning a general HTTP error. For example:
150 # ryan = Person.find(my_id)
151 # rescue ActiveResource::ResourceNotFound
152 # redirect_to :action => 'not_found'
153 # rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
154 # redirect_to :action => 'new'
157 # === Validation errors
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 # a response code of <tt>422</tt> and an XML representation of the validation errors. The save operation will
162 # then fail (with a <tt>false</tt> return value) and the validation errors can be accessed on the resource in question.
164 # ryan = Person.find(1)
166 # ryan.save # => false
169 # # PUT http://api.people.com:3000/people/1.xml
170 # # is requested with invalid values, the response is:
173 # # <errors type="array"><error>First cannot be empty</error></errors>
176 # ryan.errors.invalid?(:first) # => true
177 # ryan.errors.full_messages # => ['First cannot be empty']
179 # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
183 # Active Resource relies on HTTP to access RESTful APIs and as such is inherently susceptible to slow or
184 # unresponsive servers. In such cases, your Active Resource method calls could \timeout. You can control the
185 # amount of time before Active Resource times out with the +timeout+ variable.
187 # class Person < ActiveResource::Base
188 # self.site = "http://api.people.com:3000/"
192 # This sets the +timeout+ to 5 seconds. You can adjust the +timeout+ to a value suitable for the RESTful API
193 # you are accessing. It is recommended to set this to a reasonably low value to allow your Active Resource
194 # clients (especially if you are using Active Resource in a Rails application) to fail-fast (see
195 # http://en.wikipedia.org/wiki/Fail-fast) rather than cause cascading failures that could incapacitate your
198 # When a \timeout occurs, an ActiveResource::TimeoutError is raised. You should rescue from
199 # ActiveResource::TimeoutError in your Active Resource method calls.
201 # Internally, Active Resource relies on Ruby's Net::HTTP library to make HTTP requests. Setting +timeout+
202 # sets the <tt>read_timeout</tt> of the internal Net::HTTP instance to the same value. The default
203 # <tt>read_timeout</tt> is 60 seconds on most Ruby implementations.
205 # The logger for diagnosing and tracing Active Resource calls.
206 cattr_accessor
:logger
209 # Gets the URI of the REST resources to map for this class. The site variable is required for
210 # Active Resource's mapping to work.
212 # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
214 # With superclass_delegating_reader
216 # Parent.site = 'http://anonymous@test.com'
217 # Subclass.site # => 'http://anonymous@test.com'
218 # Subclass.site.user = 'david'
219 # Parent.site # => 'http://david@test.com'
221 # Without superclass_delegating_reader (expected behaviour)
223 # Parent.site = 'http://anonymous@test.com'
224 # Subclass.site # => 'http://anonymous@test.com'
225 # Subclass.site.user = 'david' # => TypeError: can't modify frozen object
229 elsif superclass
!= Object
&& superclass
.site
230 superclass
.site
.dup
.freeze
234 # Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
235 # The site variable is required for Active Resource's mapping to work.
241 @site = create_site_uri_from(site
)
242 @user = URI
.decode(@site.user
) if @site.user
243 @password = URI
.decode(@site.password
) if @site.password
247 # Gets the \user for REST HTTP authentication.
249 # Not using superclass_delegating_reader. See +site+ for explanation
252 elsif superclass
!= Object
&& superclass
.user
253 superclass
.user
.dup
.freeze
257 # Sets the \user for REST HTTP authentication.
263 # Gets the \password for REST HTTP authentication.
265 # Not using superclass_delegating_reader. See +site+ for explanation
266 if defined?(@password)
268 elsif superclass
!= Object
&& superclass
.password
269 superclass
.password
.dup
.freeze
273 # Sets the \password for REST HTTP authentication.
274 def password
=(password
)
279 # Sets the format that attributes are sent and received in from a mime type reference:
281 # Person.format = :json
282 # Person.find(1) # => GET /people/1.json
284 # Person.format = ActiveResource::Formats::XmlFormat
285 # Person.find(1) # => GET /people/1.xml
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 ActiveResource
::Formats[mime_type_reference_or_format
] : mime_type_reference_or_format
292 write_inheritable_attribute(:format, format
)
293 connection
.format
= format
if site
296 # Returns the current format, default is ActiveResource::Formats::XmlFormat.
298 read_inheritable_attribute(:format) || ActiveResource
::Formats[:xml]
301 # Sets the number of seconds after which requests to the REST API should time out.
302 def timeout
=(timeout
)
307 # Gets the number of seconds after which requests to the REST API should time out.
309 if defined?(@timeout)
311 elsif superclass
!= Object
&& superclass
.timeout
316 # An instance of ActiveResource::Connection that is the base \connection to the remote service.
317 # 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 if defined?(@connection) || superclass
== Object
321 @connection = Connection
.new(site
, format
) if refresh
|| @connection.nil?
322 @connection.user
= user
if user
323 @connection.password
= password
if password
324 @connection.timeout
= timeout
if timeout
327 superclass
.connection
335 # Do not include any modules in the default element name. This makes it easier to seclude ARes objects
336 # in a separate namespace without having to set element_name repeatedly.
337 attr_accessor_with_default(:element_name) { to_s
.split("::").last
.underscore
} #:nodoc:
339 attr_accessor_with_default(:collection_name) { element_name
.pluralize
} #:nodoc:
340 attr_accessor_with_default(:primary_key, 'id') #:nodoc:
342 # Gets the \prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
343 # This method is regenerated at runtime based on what the \prefix is set to.
344 def prefix(options
={})
346 default
<< '/' unless default
[-1..-1] == '/'
347 # generate the actual method based on the current site path
348 self.prefix
= default
352 # An attribute reader for the source string for the resource path \prefix. This
353 # method is regenerated at runtime based on what the \prefix is set to.
355 prefix
# generate #prefix and #prefix_source methods first
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}]}" }
365 # Clear prefix parameters in case they have been cached
366 @prefix_parameters = nil
368 # Redefine the new methods.
370 def prefix_source() "#{value}" end
371 def prefix(options={}) "#{prefix_call}" end
373 silence_warnings
{ instance_eval code
, __FILE__
, __LINE__
}
375 logger
.error
"Couldn't set prefix: #{$!}\n #{code}"
379 alias_method
:set_prefix, :prefix= #:nodoc:
381 alias_method
:set_element_name, :element_name= #:nodoc:
382 alias_method
:set_collection_name, :collection_name= #:nodoc:
384 # Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
385 # will split from the \prefix options.
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>).
390 # +query_options+ - A \hash to add items to the query string for the request.
393 # Post.element_path(1)
396 # Comment.element_path(1, :post_id => 5)
397 # # => /posts/5/comments/1.xml
399 # Comment.element_path(1, :post_id => 5, :active => 1)
400 # # => /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
405 def element_path(id
, prefix_options
= {}, query_options
= nil)
406 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)}"
410 # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
411 # will split from the +prefix_options+.
414 # * +prefix_options+ - A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
415 # would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
416 # * +query_options+ - A hash to add items to the query string for the request.
419 # Post.collection_path
422 # Comment.collection_path(:post_id => 5)
423 # # => /posts/5/comments.xml
425 # Comment.collection_path(:post_id => 5, :active => 1)
426 # # => /posts/5/comments.xml?active=1
428 # Comment.collection_path({:post_id => 5}, {:active => 1})
429 # # => /posts/5/comments.xml?active=1
431 def collection_path(prefix_options
= {}, query_options
= nil)
432 prefix_options
, query_options
= split_options(prefix_options
) if query_options
.nil?
433 "#{prefix(prefix_options)}#{collection_name}.#{format.extension}#{query_string(query_options)}"
436 alias_method
:set_primary_key, :primary_key= #:nodoc:
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:
441 # ryan = Person.new(:first => 'ryan')
444 # Returns the newly created resource. If a failure has occurred an
445 # exception will be raised (see <tt>save</tt>). If the resource is invalid and
446 # has not been saved then <tt>valid?</tt> will return <tt>false</tt>,
447 # while <tt>new?</tt> will still return <tt>true</tt>.
450 # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
451 # my_person = Person.find(:first)
452 # my_person.email # => myname@nospam.com
454 # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
455 # dhh.valid? # => true
456 # dhh.new? # => false
458 # # We'll assume that there's a validation that requires the name attribute
459 # that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
460 # that_guy.valid? # => false
461 # that_guy.new? # => true
462 def create(attributes
= {})
463 returning(self.new(attributes
)) { |res
| res
.save
}
466 # Core method for finding resources. Used similarly to Active Record's +find+ method.
469 # The first argument is considered to be the scope of the query. That is, how many
470 # resources are returned from the request. It can be one of the following.
472 # * <tt>:one</tt> - Returns a single resource.
473 # * <tt>:first</tt> - Returns the first resource found.
474 # * <tt>:last</tt> - Returns the last resource found.
475 # * <tt>:all</tt> - Returns every resource that matches the request.
479 # * <tt>:from</tt> - Sets the path or custom method that resources will be fetched from.
480 # * <tt>:params</tt> - Sets query and \prefix (nested URL) parameters.
484 # # => GET /people/1.xml
487 # # => GET /people.xml
489 # Person.find(:all, :params => { :title => "CEO" })
490 # # => GET /people.xml?title=CEO
492 # Person.find(:first, :from => :managers)
493 # # => GET /people/managers.xml
495 # Person.find(:last, :from => :managers)
496 # # => GET /people/managers.xml
498 # Person.find(:all, :from => "/companies/1/people.xml")
499 # # => GET /companies/1/people.xml
501 # Person.find(:one, :from => :leader)
502 # # => GET /people/leader.xml
504 # Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
505 # # => GET /people/developers.xml?language=ruby
507 # Person.find(:one, :from => "/companies/1/manager.xml")
508 # # => GET /companies/1/manager.xml
510 # StreetAddress.find(1, :params => { :person_id => 1 })
511 # # => GET /people/1/street_addresses/1.xml
513 scope
= arguments
.slice
!(0)
514 options
= arguments
.slice
!(0) || {}
517 when :all then find_every(options
)
518 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
)
525 # Deletes the resources with the ID in the +id+ parameter.
528 # All options specify \prefix and query parameters.
531 # Event.delete(2) # sends DELETE /events/2
533 # Event.create(:name => 'Free Concert', :location => 'Community Center')
534 # my_event = Event.find(:first) # let's assume this is event with ID 7
535 # Event.delete(my_event.id) # sends DELETE /events/7
537 # # Let's assume a request to events/5/cancel.xml
538 # Event.delete(params[:id]) # sends DELETE /events/5
539 def delete(id
, options
= {})
540 connection
.delete(element_path(id
, options
))
543 # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
546 # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
547 # Note.exists?(1) # => true
549 # Note.exists(1349) # => false
550 def exists
?(id
, options
= {})
552 prefix_options
, query_options
= split_options(options
[:params])
553 path
= element_path(id
, prefix_options
, query_options
)
554 response
= connection
.head(path
, headers
)
555 response
.code
.to_i
== 200
557 # id && !find_single(id, options).nil?
558 rescue ActiveResource
::ResourceNotFound
563 # Find every resource
564 def find_every(options
)
565 case from
= options
[:from]
567 instantiate_collection(get(from
, options
[:params]))
569 path
= "#{from}#{query_string(options[:params])}"
570 instantiate_collection(connection
.get(path
, headers
) || [])
572 prefix_options
, query_options
= split_options(options
[:params])
573 path
= collection_path(prefix_options
, query_options
)
574 instantiate_collection( (connection
.get(path
, headers
) || []), prefix_options
)
578 # Find a single resource from a one-off URL
579 def find_one(options
)
580 case from
= options
[:from]
582 instantiate_record(get(from
, options
[:params]))
584 path
= "#{from}#{query_string(options[:params])}"
585 instantiate_record(connection
.get(path
, headers
))
589 # Find a single resource from the default URL
590 def find_single(scope
, options
)
591 prefix_options
, query_options
= split_options(options
[:params])
592 path
= element_path(scope
, prefix_options
, query_options
)
593 instantiate_record(connection
.get(path
, headers
), prefix_options
)
596 def instantiate_collection(collection
, prefix_options
= {})
597 collection
.collect
! { |record
| instantiate_record(record
, prefix_options
) }
600 def instantiate_record(record
, prefix_options
= {})
601 returning
new(record
) do |resource
|
602 resource
.prefix_options
= prefix_options
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
)
612 # contains a set of the current prefix parameters.
613 def prefix_parameters
614 @prefix_parameters ||= prefix_source
.scan(/:\w+/).map
{ |key
| key
[1..-1].to_sym
}.to_set
617 # Builds the query string for the request.
618 def query_string(options
)
619 "?#{options.to_query}" unless options
.nil? || options
.empty
?
622 # split an option hash into two hashes, one containing the prefix options,
623 # and the other containing the leftovers.
624 def split_options(options
= {})
625 prefix_options
, query_options
= {}, {}
627 (options
|| {}).each
do |key
, value
|
629 (prefix_parameters
.include?(key
.to_sym
) ? prefix_options
: query_options
)[key
.to_sym
] = value
632 [ prefix_options
, query_options
]
636 attr_accessor
:attributes #:nodoc:
637 attr_accessor
:prefix_options #:nodoc:
639 # Constructor method for \new resources; the optional +attributes+ parameter takes a \hash
640 # of attributes for the \new resource.
643 # my_course = Course.new
644 # my_course.name = "Western Civilization"
645 # my_course.lecturer = "Don Trotter"
648 # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
649 # my_other_course.save
650 def initialize(attributes
= {})
656 # Returns a \clone of the resource that hasn't been assigned an +id+ yet and
657 # is treated as a \new resource.
659 # ryan = Person.find(1)
660 # not_ryan = ryan.clone
661 # not_ryan.new? # => true
663 # Any active resource member attributes will NOT be cloned, though all other
664 # attributes are. This is to prevent the conflict between any +prefix_options+
665 # that refer to the original parent resource and the newly cloned parent
666 # resource that does not exist.
668 # ryan = Person.find(1)
669 # ryan.address = StreetAddress.find(1, :person_id => ryan.id)
670 # ryan.hash = {:not => "an ARes instance"}
672 # not_ryan = ryan.clone
673 # not_ryan.new? # => true
674 # not_ryan.address # => NoMethodError
675 # not_ryan.hash # => {:not => "an ARes instance"}
677 # Clone all attributes except the pk and any nested ARes
678 cloned
= attributes
.reject
{|k
,v
| k
== self.class.primary_key
|| v
.is_a
?(ActiveResource
::Base)}.inject({}) do |attrs
, (k
, v
)|
682 # Form the new resource - bypass initialize of resource with 'new' as that will call 'load' which
683 # attempts to convert hashes into member objects and arrays into collections of objects. We want
684 # the raw objects to be cloned so we bypass load by directly setting the attributes hash.
685 resource
= self.class.new({})
686 resource
.prefix_options
= self.prefix_options
687 resource
.send
:instance_variable_set, '@attributes', cloned
692 # A method to determine if the resource a \new object (i.e., it has not been POSTed to the remote service yet).
695 # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
696 # not_new.new? # => false
698 # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
699 # is_new.new? # => true
702 # is_new.new? # => false
708 # Gets the <tt>\id</tt> attribute of the resource.
710 attributes
[self.class.primary_key
]
713 # Sets the <tt>\id</tt> attribute of the resource.
715 attributes
[self.class.primary_key
] = id
718 # Allows Active Resource objects to be used as parameters in Action Pack URL generation.
723 # Test for equality. Resource are equal if and only if +other+ is the same object or
724 # is an instance of the same class, is not <tt>new?</tt>, and has the same +id+.
727 # ryan = Person.create(:name => 'Ryan')
728 # jamie = Person.create(:name => 'Jamie')
731 # # => false (Different name attribute and id)
733 # ryan_again = Person.new(:name => 'Ryan')
735 # # => false (ryan_again is new?)
737 # ryans_clone = Person.create(:name => 'Ryan')
738 # ryan == ryans_clone
739 # # => false (Different id attributes)
741 # ryans_twin = Person.find(ryan.id)
746 other
.equal
?(self) || (other
.instance_of
?(self.class) && !other
.new
? && other
.id
== id
)
749 # Tests for equality (delegates to ==).
754 # Delegates to id in order to allow two resources of the same type and \id to work with something like:
755 # [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
760 # Duplicate the current resource without saving it.
763 # my_invoice = Invoice.create(:customer => 'That Company')
764 # next_invoice = my_invoice.dup
765 # next_invoice.new? # => true
768 # next_invoice == my_invoice # => false (different id attributes)
770 # my_invoice.customer # => That Company
771 # next_invoice.customer # => That Company
773 returning
self.class.new
do |resource
|
774 resource
.attributes
= @attributes
775 resource
.prefix_options
= @prefix_options
779 # A method to \save (+POST+) or \update (+PUT+) a resource. It delegates to +create+ if a \new object,
780 # +update+ if it is existing. If the response to the \save includes a body, it will be assumed that this body
781 # is XML for the final object as it looked after the \save (which would include attributes like +created_at+
782 # that weren't part of the original submit).
785 # my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
786 # my_company.new? # => true
787 # my_company.save # sends POST /companies/ (create)
789 # my_company.new? # => false
790 # my_company.size = 10
791 # my_company.save # sends PUT /companies/1 (update)
793 new
? ? create
: update
796 # Deletes the resource from the remote service.
800 # my_person = Person.find(my_id)
802 # Person.find(my_id) # 404 (Resource Not Found)
804 # new_person = Person.create(:name => 'James')
805 # new_id = new_person.id # => 7
807 # Person.find(new_id) # 404 (Resource Not Found)
809 connection
.delete(element_path
, self.class.headers
)
812 # Evaluates to <tt>true</tt> if this resource is not <tt>new?</tt> and is
813 # found on the remote service. Using this method, you can check for
814 # resources that may have been deleted between the object's instantiation
818 # Person.create(:name => 'Theodore Roosevelt')
819 # that_guy = Person.find(:first)
820 # that_guy.exists? # => true
822 # that_lady = Person.new(:name => 'Paul Bean')
823 # that_lady.exists? # => false
825 # guys_id = that_guy.id
826 # Person.delete(guys_id)
827 # that_guy.exists? # => false
829 !new
? && self.class.exists
?(to_param
, :params => prefix_options
)
832 # A method to convert the the resource to an XML string.
835 # The +options+ parameter is handed off to the +to_xml+ method on each
836 # attribute, so it has the same options as the +to_xml+ methods in
839 # * <tt>:indent</tt> - Set the indent level for the XML output (default is +2+).
840 # * <tt>:dasherize</tt> - Boolean option to determine whether or not element names should
841 # replace underscores with dashes (default is <tt>false</tt>).
842 # * <tt>:skip_instruct</tt> - Toggle skipping the +instruct!+ call on the XML builder
843 # that generates the XML declaration (default is <tt>false</tt>).
846 # my_group = SubsidiaryGroup.find(:first)
848 # # => <?xml version="1.0" encoding="UTF-8"?>
849 # # <subsidiary_group> [...] </subsidiary_group>
851 # my_group.to_xml(:dasherize => true)
852 # # => <?xml version="1.0" encoding="UTF-8"?>
853 # # <subsidiary-group> [...] </subsidiary-group>
855 # my_group.to_xml(:skip_instruct => true)
856 # # => <subsidiary_group> [...] </subsidiary_group>
857 def to_xml(options
={})
858 attributes
.to_xml({:root => self.class.element_name
}.merge(options
))
861 # Returns a JSON string representing the model. Some configuration is
862 # available through +options+.
865 # The +options+ are passed to the +to_json+ method on each
866 # attribute, so the same options as the +to_json+ methods in
869 # * <tt>:only</tt> - Only include the specified attribute or list of
870 # attributes in the serialized output. Attribute names must be specified
872 # * <tt>:except</tt> - Do not include the specified attribute or list of
873 # attributes in the serialized output. Attribute names must be specified
877 # person = Person.new(:first_name => "Jim", :last_name => "Smith")
879 # # => {"first_name": "Jim", "last_name": "Smith"}
881 # person.to_json(:only => ["first_name"])
882 # # => {"first_name": "Jim"}
884 # person.to_json(:except => ["first_name"])
885 # # => {"last_name": "Smith"}
886 def to_json(options
={})
887 attributes
.to_json(options
)
890 # Returns the serialized string representation of the resource in the configured
891 # serialization format specified in ActiveResource::Base.format. The options
892 # applicable depend on the configured encoding format.
893 def encode(options
={})
894 case self.class.format
895 when ActiveResource
::Formats[:xml]
896 self.class.format
.encode(attributes
, {:root => self.class.element_name
}.merge(options
))
898 self.class.format
.encode(attributes
, options
)
902 # A method to \reload the attributes of this object from the remote web service.
905 # my_branch = Branch.find(:first)
906 # my_branch.name # => "Wislon Raod"
908 # # Another client fixes the typo...
910 # my_branch.name # => "Wislon Raod"
912 # my_branch.name # => "Wilson Road"
914 self.load(self.class.find(to_param
, :params => @prefix_options).attributes
)
917 # A method to manually load attributes from a \hash. Recursively loads collections of
918 # resources. This method is called in +initialize+ and +create+ when a \hash of attributes
922 # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
923 # my_attrs = {:name => 'Marty', :colors => ["red", "green", "blue"]}
925 # the_supplier = Supplier.find(:first)
926 # the_supplier.name # => 'J&M Textiles'
927 # the_supplier.load(my_attrs)
928 # the_supplier.name('J&J Textiles')
930 # # These two calls are the same as Supplier.new(my_attrs)
931 # my_supplier = Supplier.new
932 # my_supplier.load(my_attrs)
934 # # These three calls are the same as Supplier.create(my_attrs)
935 # your_supplier = Supplier.new
936 # your_supplier.load(my_attrs)
939 raise ArgumentError
, "expected an attributes Hash, got #{attributes.inspect}" unless attributes
.is_a
?(Hash
)
940 @prefix_options, attributes
= split_options(attributes
)
941 attributes
.each
do |key
, value
|
942 @attributes[key
.to_s
] =
945 resource
= find_or_create_resource_for_collection(key
)
946 value
.map
{ |attrs
| attrs
.is_a
?(String
) ? attrs
.dup
: resource
.new(attrs
) }
948 resource
= find_or_create_resource_for(key
)
951 value
.dup
rescue value
957 # For checking <tt>respond_to?</tt> without searching the attributes (which is faster).
958 alias_method
:respond_to_without_attributes?, :respond_to?
960 # A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a Person object with a
961 # +name+ attribute can answer <tt>true</tt> to <tt>my_person.respond_to?(:name)</tt>, <tt>my_person.respond_to?(:name=)</tt>, and
962 # <tt>my_person.respond_to?(:name?)</tt>.
963 def respond_to
?(method
, include_priv
= false)
964 method_name
= method
.to_s
967 elsif attributes
.has_key
?(method_name
)
969 elsif ['?','='].include?(method_name
.last
) && attributes
.has_key
?(method_name
.first(-1))
972 # super must be called at the end of the method, because the inherited respond_to?
973 # would return true for generated readers, even if the attribute wasn't present
979 def connection(refresh
= false)
980 self.class.connection(refresh
)
983 # Update the resource on the remote service.
985 returning connection
.put(element_path(prefix_options
), encode
, self.class.headers
) do |response
|
986 load_attributes_from_response(response
)
990 # Create (i.e., \save to the remote service) the \new resource.
992 returning connection
.post(collection_path
, encode
, self.class.headers
) do |response
|
993 self.id
= id_from_response(response
)
994 load_attributes_from_response(response
)
998 def load_attributes_from_response(response
)
999 if response
['Content-Length'] != "0" && response
.body
.strip
.size
> 0
1000 load(self.class.format
.decode(response
.body
))
1004 # Takes a response from a typical create post and pulls the ID out
1005 def id_from_response(response
)
1006 response
['Location'][/\/([^\
/]*?)(\.\w+)?$/, 1]
1009 def element_path(options
= nil)
1010 self.class.element_path(to_param
, options
|| prefix_options
)
1013 def collection_path(options
= nil)
1014 self.class.collection_path(options
|| prefix_options
)
1018 # Tries to find a resource for a given collection name; if it fails, then the resource is created
1019 def find_or_create_resource_for_collection(name
)
1020 find_or_create_resource_for(name
.to_s
.singularize
)
1023 # Tries to find a resource in a non empty list of nested modules
1024 # Raises a NameError if it was not found in any of the given nested modules
1025 def find_resource_in_modules(resource_name
, module_names
)
1027 namespaces
= module_names
[0, module_names
.size-1
].map
do |module_name
|
1028 receiver
= receiver
.const_get(module_name
)
1030 if namespace
= namespaces
.reverse
.detect
{ |ns
| ns
.const_defined
?(resource_name
) }
1031 return namespace
.const_get(resource_name
)
1037 # Tries to find a resource for a given name; if it fails, then the resource is created
1038 def find_or_create_resource_for(name
)
1039 resource_name
= name
.to_s
.camelize
1040 ancestors
= self.class.name
.split("::")
1041 if ancestors
.size
> 1
1042 find_resource_in_modules(resource_name
, ancestors
)
1044 self.class.const_get(resource_name
)
1047 if self.class.const_defined
?(resource_name
)
1048 resource
= self.class.const_get(resource_name
)
1050 resource
= self.class.const_set(resource_name
, Class
.new(ActiveResource
::Base))
1052 resource
.prefix
= self.class.prefix
1053 resource
.site
= self.class.site
1057 def split_options(options
= {})
1058 self.class.__send__(:split_options, options
)
1061 def method_missing(method_symbol
, *arguments
) #:nodoc:
1062 method_name
= method_symbol
.to_s
1064 case method_name
.last
1066 attributes
[method_name
.first(-1)] = arguments
.first
1068 attributes
[method_name
.first(-1)]
1070 attributes
.has_key
?(method_name
) ? attributes
[method_name
] : super