Froze rails gems
[depot.git] / vendor / rails / activeresource / lib / active_resource / base.rb
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. 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.
24 #
25 # class PersonResource < ActiveResource::Base
26 # self.site = "http://api.people.com:3000/"
27 # self.element_name = "person"
28 # end
29 #
30 #
31 # == Lifecycle methods
32 #
33 # Active Resource exposes methods for creating, finding, updating, and deleting resources
34 # from REST web services.
35 #
36 # ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
37 # ryan.save # => true
38 # ryan.id # => 2
39 # Person.exists?(ryan.id) # => true
40 # ryan.exists? # => true
41 #
42 # ryan = Person.find(1)
43 # # Resource holding our newly created Person object
44 #
45 # ryan.first = 'Rizzle'
46 # ryan.save # => true
47 #
48 # ryan.destroy # => true
49 #
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.
52 #
53 # === Custom REST methods
54 #
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
58 # name to invoke.
59 #
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' }
63 #
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 # # GET all the positions available, i.e. GET /people/positions.xml.
69 # Person.get(:positions)
70 # # => [{:name => 'Manager'}, {:name => 'Clerk'}]
71 #
72 # # DELETE to 'fire' a person, i.e. DELETE /people/1/fire.xml.
73 # Person.find(1).delete(:fire)
74 #
75 # For more information on using custom REST methods, see the
76 # ActiveResource::CustomMethods documentation.
77 #
78 # == Validations
79 #
80 # You can validate resources client side by overriding validation methods in the base class.
81 #
82 # class Person < ActiveResource::Base
83 # self.site = "http://api.people.com:3000/"
84 # protected
85 # def validate
86 # errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
87 # end
88 # end
89 #
90 # See the ActiveResource::Validations documentation for more information.
91 #
92 # == Authentication
93 #
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.
97 #
98 # class Person < ActiveResource::Base
99 # self.site = "http://ryan:password@api.people.com:3000/"
100 # end
101 #
102 # * defining +user+ and/or +password+ variables
103 #
104 # class Person < ActiveResource::Base
105 # self.site = "http://api.people.com:3000/"
106 # self.user = "ryan"
107 # self.password = "password"
108 # end
109 #
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
114 # as usernames. In those situations you should use the separate user and password option.
115 # == Errors & Validation
116 #
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.
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 # response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
125 # exception.
126 #
127 # # GET http://api.people.com:3000/people/999.xml
128 # ryan = Person.find(999) # 404, raises ActiveResource::ResourceNotFound
129 #
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:
132 #
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
145 #
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:
148 #
149 # begin
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'
155 # end
156 #
157 # === 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 # 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.
163 #
164 # ryan = Person.find(1)
165 # ryan.first # => ''
166 # ryan.save # => false
167 #
168 # # When
169 # # PUT http://api.people.com:3000/people/1.xml
170 # # is requested with invalid values, the response is:
171 # #
172 # # Response (422):
173 # # <errors type="array"><error>First cannot be empty</error></errors>
174 # #
175 #
176 # ryan.errors.invalid?(:first) # => true
177 # ryan.errors.full_messages # => ['First cannot be empty']
178 #
179 # Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
180 #
181 # === Timeouts
182 #
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.
186 #
187 # class Person < ActiveResource::Base
188 # self.site = "http://api.people.com:3000/"
189 # self.timeout = 5
190 # end
191 #
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
196 # server.
197 #
198 # When a \timeout occurs, an ActiveResource::TimeoutError is raised. You should rescue from
199 # ActiveResource::TimeoutError in your Active Resource method calls.
200 #
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.
204 class Base
205 # The logger for diagnosing and tracing Active Resource calls.
206 cattr_accessor :logger
207
208 class << self
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.
211 def site
212 # Not using superclass_delegating_reader because don't want subclasses to modify superclass instance
213 #
214 # With superclass_delegating_reader
215 #
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'
220 #
221 # Without superclass_delegating_reader (expected behaviour)
222 #
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
226 #
227 if defined?(@site)
228 @site
229 elsif superclass != Object && superclass.site
230 superclass.site.dup.freeze
231 end
232 end
233
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.
236 def site=(site)
237 @connection = nil
238 if site.nil?
239 @site = nil
240 else
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
244 end
245 end
246
247 # Gets the \user for REST HTTP authentication.
248 def user
249 # Not using superclass_delegating_reader. See +site+ for explanation
250 if defined?(@user)
251 @user
252 elsif superclass != Object && superclass.user
253 superclass.user.dup.freeze
254 end
255 end
256
257 # Sets the \user for REST HTTP authentication.
258 def user=(user)
259 @connection = nil
260 @user = user
261 end
262
263 # Gets the \password for REST HTTP authentication.
264 def password
265 # Not using superclass_delegating_reader. See +site+ for explanation
266 if defined?(@password)
267 @password
268 elsif superclass != Object && superclass.password
269 superclass.password.dup.freeze
270 end
271 end
272
273 # Sets the \password for REST HTTP authentication.
274 def password=(password)
275 @connection = nil
276 @password = password
277 end
278
279 # Sets the format that attributes are sent and received in from a mime type reference:
280 #
281 # Person.format = :json
282 # Person.find(1) # => GET /people/1.json
283 #
284 # Person.format = ActiveResource::Formats::XmlFormat
285 # Person.find(1) # => GET /people/1.xml
286 #
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
291
292 write_inheritable_attribute(:format, format)
293 connection.format = format if site
294 end
295
296 # Returns the current format, default is ActiveResource::Formats::XmlFormat.
297 def format
298 read_inheritable_attribute(:format) || ActiveResource::Formats[:xml]
299 end
300
301 # Sets the number of seconds after which requests to the REST API should time out.
302 def timeout=(timeout)
303 @connection = nil
304 @timeout = timeout
305 end
306
307 # Gets the number of seconds after which requests to the REST API should time out.
308 def timeout
309 if defined?(@timeout)
310 @timeout
311 elsif superclass != Object && superclass.timeout
312 superclass.timeout
313 end
314 end
315
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
325 @connection
326 else
327 superclass.connection
328 end
329 end
330
331 def headers
332 @headers ||= {}
333 end
334
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:
338
339 attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
340 attr_accessor_with_default(:primary_key, 'id') #:nodoc:
341
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={})
345 default = site.path
346 default << '/' unless default[-1..-1] == '/'
347 # generate the actual method based on the current site path
348 self.prefix = default
349 prefix(options)
350 end
351
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.
354 def prefix_source
355 prefix # generate #prefix and #prefix_source methods first
356 prefix_source
357 end
358
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 # Clear prefix parameters in case they have been cached
366 @prefix_parameters = nil
367
368 # Redefine the new methods.
369 code = <<-end_code
370 def prefix_source() "#{value}" end
371 def prefix(options={}) "#{prefix_call}" end
372 end_code
373 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 alias_method :set_element_name, :element_name= #:nodoc:
382 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
385 # will split from the \prefix options.
386 #
387 # ==== 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.
391 #
392 # ==== Examples
393 # Post.element_path(1)
394 # # => /posts/1.xml
395 #
396 # Comment.element_path(1, :post_id => 5)
397 # # => /posts/5/comments/1.xml
398 #
399 # Comment.element_path(1, :post_id => 5, :active => 1)
400 # # => /posts/5/comments/1.xml?active=1
401 #
402 # Comment.element_path(1, {:post_id => 5}, {:active => 1})
403 # # => /posts/5/comments/1.xml?active=1
404 #
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)}"
408 end
409
410 # Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
411 # will split from the +prefix_options+.
412 #
413 # ==== 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.
417 #
418 # ==== Examples
419 # Post.collection_path
420 # # => /posts.xml
421 #
422 # Comment.collection_path(:post_id => 5)
423 # # => /posts/5/comments.xml
424 #
425 # Comment.collection_path(:post_id => 5, :active => 1)
426 # # => /posts/5/comments.xml?active=1
427 #
428 # Comment.collection_path({:post_id => 5}, {:active => 1})
429 # # => /posts/5/comments.xml?active=1
430 #
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)}"
434 end
435
436 alias_method :set_primary_key, :primary_key= #:nodoc:
437
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 # ryan = Person.new(:first => 'ryan')
442 # ryan.save
443 #
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>.
448 #
449 # ==== Examples
450 # Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
451 # my_person = Person.find(:first)
452 # my_person.email # => myname@nospam.com
453 #
454 # dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
455 # dhh.valid? # => true
456 # dhh.new? # => false
457 #
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 }
464 end
465
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 # resources are returned from the request. It can be one of the following.
471 #
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.
476 #
477 # ==== Options
478 #
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.
481 #
482 # ==== Examples
483 # Person.find(1)
484 # # => GET /people/1.xml
485 #
486 # Person.find(:all)
487 # # => GET /people.xml
488 #
489 # Person.find(:all, :params => { :title => "CEO" })
490 # # => GET /people.xml?title=CEO
491 #
492 # Person.find(:first, :from => :managers)
493 # # => GET /people/managers.xml
494 #
495 # Person.find(:last, :from => :managers)
496 # # => GET /people/managers.xml
497 #
498 # Person.find(:all, :from => "/companies/1/people.xml")
499 # # => GET /companies/1/people.xml
500 #
501 # Person.find(:one, :from => :leader)
502 # # => GET /people/leader.xml
503 #
504 # Person.find(:all, :from => :developers, :params => { :language => 'ruby' })
505 # # => GET /people/developers.xml?language=ruby
506 #
507 # Person.find(:one, :from => "/companies/1/manager.xml")
508 # # => GET /companies/1/manager.xml
509 #
510 # StreetAddress.find(1, :params => { :person_id => 1 })
511 # # => GET /people/1/street_addresses/1.xml
512 def find(*arguments)
513 scope = arguments.slice!(0)
514 options = arguments.slice!(0) || {}
515
516 case scope
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)
522 end
523 end
524
525 # Deletes the resources with the ID in the +id+ parameter.
526 #
527 # ==== Options
528 # All options specify \prefix and query parameters.
529 #
530 # ==== Examples
531 # Event.delete(2) # sends DELETE /events/2
532 #
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
536 #
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))
541 end
542
543 # Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
544 #
545 # ==== Examples
546 # Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
547 # Note.exists?(1) # => true
548 #
549 # Note.exists(1349) # => false
550 def exists?(id, options = {})
551 if id
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
556 end
557 # id && !find_single(id, options).nil?
558 rescue ActiveResource::ResourceNotFound
559 false
560 end
561
562 private
563 # Find every resource
564 def find_every(options)
565 case from = options[:from]
566 when Symbol
567 instantiate_collection(get(from, options[:params]))
568 when String
569 path = "#{from}#{query_string(options[:params])}"
570 instantiate_collection(connection.get(path, headers) || [])
571 else
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 )
575 end
576 end
577
578 # Find a single resource from a one-off URL
579 def find_one(options)
580 case from = options[:from]
581 when Symbol
582 instantiate_record(get(from, options[:params]))
583 when String
584 path = "#{from}#{query_string(options[:params])}"
585 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 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)
594 end
595
596 def instantiate_collection(collection, prefix_options = {})
597 collection.collect! { |record| instantiate_record(record, prefix_options) }
598 end
599
600 def instantiate_record(record, prefix_options = {})
601 returning new(record) do |resource|
602 resource.prefix_options = prefix_options
603 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 def prefix_parameters
614 @prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
615 end
616
617 # Builds the query string for the request.
618 def query_string(options)
619 "?#{options.to_query}" unless options.nil? || options.empty?
620 end
621
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 = {}, {}
626
627 (options || {}).each do |key, value|
628 next if key.blank?
629 (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 end
635
636 attr_accessor :attributes #:nodoc:
637 attr_accessor :prefix_options #:nodoc:
638
639 # Constructor method for \new resources; the optional +attributes+ parameter takes a \hash
640 # of attributes for the \new resource.
641 #
642 # ==== Examples
643 # my_course = Course.new
644 # my_course.name = "Western Civilization"
645 # my_course.lecturer = "Don Trotter"
646 # my_course.save
647 #
648 # my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
649 # my_other_course.save
650 def initialize(attributes = {})
651 @attributes = {}
652 @prefix_options = {}
653 load(attributes)
654 end
655
656 # Returns a \clone of the resource that hasn't been assigned an +id+ yet and
657 # is treated as a \new resource.
658 #
659 # ryan = Person.find(1)
660 # not_ryan = ryan.clone
661 # not_ryan.new? # => true
662 #
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.
667 #
668 # ryan = Person.find(1)
669 # ryan.address = StreetAddress.find(1, :person_id => ryan.id)
670 # ryan.hash = {:not => "an ARes instance"}
671 #
672 # not_ryan = ryan.clone
673 # not_ryan.new? # => true
674 # not_ryan.address # => NoMethodError
675 # not_ryan.hash # => {:not => "an ARes instance"}
676 def clone
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)|
679 attrs[k] = v.clone
680 attrs
681 end
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
688 resource
689 end
690
691
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 # ==== Examples
695 # not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
696 # not_new.new? # => false
697 #
698 # is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
699 # is_new.new? # => true
700 #
701 # is_new.save
702 # is_new.new? # => false
703 #
704 def new?
705 id.nil?
706 end
707
708 # Gets the <tt>\id</tt> attribute of the resource.
709 def id
710 attributes[self.class.primary_key]
711 end
712
713 # Sets the <tt>\id</tt> attribute of the resource.
714 def id=(id)
715 attributes[self.class.primary_key] = id
716 end
717
718 # Allows Active Resource objects to be used as parameters in Action Pack URL generation.
719 def to_param
720 id && id.to_s
721 end
722
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+.
725 #
726 # ==== Examples
727 # ryan = Person.create(:name => 'Ryan')
728 # jamie = Person.create(:name => 'Jamie')
729 #
730 # ryan == jamie
731 # # => false (Different name attribute and id)
732 #
733 # ryan_again = Person.new(:name => 'Ryan')
734 # ryan == ryan_again
735 # # => false (ryan_again is new?)
736 #
737 # ryans_clone = Person.create(:name => 'Ryan')
738 # ryan == ryans_clone
739 # # => false (Different id attributes)
740 #
741 # ryans_twin = Person.find(ryan.id)
742 # ryan == ryans_twin
743 # # => true
744 #
745 def ==(other)
746 other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
747 end
748
749 # Tests for equality (delegates to ==).
750 def eql?(other)
751 self == other
752 end
753
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)]
756 def hash
757 id.hash
758 end
759
760 # Duplicate the current resource without saving it.
761 #
762 # ==== Examples
763 # my_invoice = Invoice.create(:customer => 'That Company')
764 # next_invoice = my_invoice.dup
765 # next_invoice.new? # => true
766 #
767 # next_invoice.save
768 # next_invoice == my_invoice # => false (different id attributes)
769 #
770 # my_invoice.customer # => That Company
771 # next_invoice.customer # => That Company
772 def dup
773 returning self.class.new do |resource|
774 resource.attributes = @attributes
775 resource.prefix_options = @prefix_options
776 end
777 end
778
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).
783 #
784 # ==== Examples
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)
788 #
789 # my_company.new? # => false
790 # my_company.size = 10
791 # my_company.save # sends PUT /companies/1 (update)
792 def save
793 new? ? create : update
794 end
795
796 # Deletes the resource from the remote service.
797 #
798 # ==== Examples
799 # my_id = 3
800 # my_person = Person.find(my_id)
801 # my_person.destroy
802 # Person.find(my_id) # 404 (Resource Not Found)
803 #
804 # new_person = Person.create(:name => 'James')
805 # new_id = new_person.id # => 7
806 # new_person.destroy
807 # Person.find(new_id) # 404 (Resource Not Found)
808 def destroy
809 connection.delete(element_path, self.class.headers)
810 end
811
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
815 # and actions on it.
816 #
817 # ==== Examples
818 # Person.create(:name => 'Theodore Roosevelt')
819 # that_guy = Person.find(:first)
820 # that_guy.exists? # => true
821 #
822 # that_lady = Person.new(:name => 'Paul Bean')
823 # that_lady.exists? # => false
824 #
825 # guys_id = that_guy.id
826 # Person.delete(guys_id)
827 # that_guy.exists? # => false
828 def exists?
829 !new? && self.class.exists?(to_param, :params => prefix_options)
830 end
831
832 # A method to convert the the resource to an XML string.
833 #
834 # ==== Options
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
837 # Active Support.
838 #
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>).
844 #
845 # ==== Examples
846 # my_group = SubsidiaryGroup.find(:first)
847 # my_group.to_xml
848 # # => <?xml version="1.0" encoding="UTF-8"?>
849 # # <subsidiary_group> [...] </subsidiary_group>
850 #
851 # my_group.to_xml(:dasherize => true)
852 # # => <?xml version="1.0" encoding="UTF-8"?>
853 # # <subsidiary-group> [...] </subsidiary-group>
854 #
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))
859 end
860
861 # Returns a JSON string representing the model. Some configuration is
862 # available through +options+.
863 #
864 # ==== 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
867 # Active Support.
868 #
869 # * <tt>:only</tt> - Only include the specified attribute or list of
870 # attributes in the serialized output. Attribute names must be specified
871 # as strings.
872 # * <tt>:except</tt> - Do not include the specified attribute or list of
873 # attributes in the serialized output. Attribute names must be specified
874 # as strings.
875 #
876 # ==== Examples
877 # person = Person.new(:first_name => "Jim", :last_name => "Smith")
878 # person.to_json
879 # # => {"first_name": "Jim", "last_name": "Smith"}
880 #
881 # person.to_json(:only => ["first_name"])
882 # # => {"first_name": "Jim"}
883 #
884 # person.to_json(:except => ["first_name"])
885 # # => {"last_name": "Smith"}
886 def to_json(options={})
887 attributes.to_json(options)
888 end
889
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))
897 else
898 self.class.format.encode(attributes, options)
899 end
900 end
901
902 # A method to \reload the attributes of this object from the remote web service.
903 #
904 # ==== Examples
905 # my_branch = Branch.find(:first)
906 # my_branch.name # => "Wislon Raod"
907 #
908 # # Another client fixes the typo...
909 #
910 # my_branch.name # => "Wislon Raod"
911 # my_branch.reload
912 # my_branch.name # => "Wilson Road"
913 def reload
914 self.load(self.class.find(to_param, :params => @prefix_options).attributes)
915 end
916
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
919 # is provided.
920 #
921 # ==== Examples
922 # my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
923 # my_attrs = {:name => 'Marty', :colors => ["red", "green", "blue"]}
924 #
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')
929 #
930 # # These two calls are the same as Supplier.new(my_attrs)
931 # my_supplier = Supplier.new
932 # my_supplier.load(my_attrs)
933 #
934 # # These three calls are the same as Supplier.create(my_attrs)
935 # your_supplier = Supplier.new
936 # your_supplier.load(my_attrs)
937 # your_supplier.save
938 def load(attributes)
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] =
943 case value
944 when Array
945 resource = find_or_create_resource_for_collection(key)
946 value.map { |attrs| attrs.is_a?(String) ? attrs.dup : resource.new(attrs) }
947 when Hash
948 resource = find_or_create_resource_for(key)
949 resource.new(value)
950 else
951 value.dup rescue value
952 end
953 end
954 self
955 end
956
957 # For checking <tt>respond_to?</tt> without searching the attributes (which is faster).
958 alias_method :respond_to_without_attributes?, :respond_to?
959
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
965 if attributes.nil?
966 return super
967 elsif attributes.has_key?(method_name)
968 return true
969 elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
970 return true
971 end
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
974 super
975 end
976
977
978 protected
979 def connection(refresh = false)
980 self.class.connection(refresh)
981 end
982
983 # Update the resource on the remote service.
984 def update
985 returning connection.put(element_path(prefix_options), encode, self.class.headers) do |response|
986 load_attributes_from_response(response)
987 end
988 end
989
990 # Create (i.e., \save to the remote service) the \new resource.
991 def create
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)
995 end
996 end
997
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))
1001 end
1002 end
1003
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]
1007 end
1008
1009 def element_path(options = nil)
1010 self.class.element_path(to_param, options || prefix_options)
1011 end
1012
1013 def collection_path(options = nil)
1014 self.class.collection_path(options || prefix_options)
1015 end
1016
1017 private
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)
1021 end
1022
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)
1026 receiver = Object
1027 namespaces = module_names[0, module_names.size-1].map do |module_name|
1028 receiver = receiver.const_get(module_name)
1029 end
1030 if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(resource_name) }
1031 return namespace.const_get(resource_name)
1032 else
1033 raise NameError
1034 end
1035 end
1036
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)
1043 else
1044 self.class.const_get(resource_name)
1045 end
1046 rescue NameError
1047 if self.class.const_defined?(resource_name)
1048 resource = self.class.const_get(resource_name)
1049 else
1050 resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
1051 end
1052 resource.prefix = self.class.prefix
1053 resource.site = self.class.site
1054 resource
1055 end
1056
1057 def split_options(options = {})
1058 self.class.__send__(:split_options, options)
1059 end
1060
1061 def method_missing(method_symbol, *arguments) #:nodoc:
1062 method_name = method_symbol.to_s
1063
1064 case method_name.last
1065 when "="
1066 attributes[method_name.first(-1)] = arguments.first
1067 when "?"
1068 attributes[method_name.first(-1)]
1069 else
1070 attributes.has_key?(method_name) ? attributes[method_name] : super
1071 end
1072 end
1073 end
1074 end