root/trunk/activeresource/lib/active_resource/base.rb
| Revision 9145, 36.5 kB (checked in by bitsweat, 3 months ago) |
|---|
| 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 |