mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Big documentation upgrade for ARes (closes #8694) [jeremymcanally]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7098 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
753cbf1cd4
commit
ae4838fff2
5 changed files with 674 additions and 189 deletions
|
@ -1,65 +1,57 @@
|
||||||
= Active Resource -- Object-oriented REST services
|
= Active Resource
|
||||||
|
|
||||||
Active Resource (ARes) connects business objects and REST web services. It is a library
|
Active Resource (ARes) connects business objects and Representational State Transfer (REST)
|
||||||
intended to provide transparent proxying capabilities between a client and a RESTful
|
web services. It implements object-relational mapping for REST webservices to provide transparent
|
||||||
service (for which Rails provides the {Simply RESTful routing}[http://dev.rubyonrails.org/browser/trunk/actionpack/lib/action_controller/resources.rb] implementation).
|
proxying capabilities between a client (ActiveResource) and a RESTful service (which is provided by Simply RESTful routing
|
||||||
|
in ActionController::Resources).
|
||||||
|
|
||||||
=== Configuration & Usage
|
== Philosophy
|
||||||
|
|
||||||
Configuration is as simple as inheriting from ActiveResource::Base and providing a site
|
Active Resource attempts to provide a coherent wrapper object-relational mapping for REST
|
||||||
class variable:
|
web services. It follows the same philosophy as Active Record, in that one of its prime aims
|
||||||
|
is to reduce the amount of code needed to map to these resources. This is made possible
|
||||||
|
by relying on a number of code- and protocol-based conventions that make it easy for Active Resource
|
||||||
|
to infer complex relations and structures. These conventions are outlined in detail in the documentation
|
||||||
|
for ActiveResource::Base.
|
||||||
|
|
||||||
|
== Overview
|
||||||
|
|
||||||
|
Model classes are mapped to remote REST resources by Active Resource much the same way Active Record maps model classes to database
|
||||||
|
tables. When a request is made to a remote resource, a REST XML request is generated, transmitted, and the result
|
||||||
|
received and serialized into a usable Ruby object.
|
||||||
|
|
||||||
|
=== Configuration and Usage
|
||||||
|
|
||||||
|
Putting ActiveResource to use is very similar to ActiveRecord. It's as simple as creating a model class
|
||||||
|
that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it:
|
||||||
|
|
||||||
class Person < ActiveResource::Base
|
class Person < ActiveResource::Base
|
||||||
self.site = "http://api.people.com:3000/"
|
self.site = "http://api.people.com:3000/"
|
||||||
end
|
end
|
||||||
|
|
||||||
Person is now REST enable and can invoke REST services very similarly to how ActiveRecord invokes
|
Now the Person class is REST enabled and can invoke REST services very similarly to how ActiveRecord invokes
|
||||||
lifecycle methods that operate against a persistent store.
|
lifecycle methods that operate against a persistent store.
|
||||||
|
|
||||||
# Find a person with id = 1
|
# Find a person with id = 1
|
||||||
# This will invoke the following Http call:
|
|
||||||
# GET http://api.people.com:3000/people/1.xml
|
|
||||||
# and will load up the XML response into a new
|
|
||||||
# Person object
|
|
||||||
#
|
|
||||||
ryan = Person.find(1)
|
ryan = Person.find(1)
|
||||||
Person.exists?(1) #=> true
|
Person.exists?(1) #=> true
|
||||||
|
|
||||||
# To create a new person - instantiate the object and call 'save',
|
As you can see, the methods are quite similar to Active Record's methods for dealing with database
|
||||||
# which will invoke this Http call:
|
records. But rather than dealing with
|
||||||
# POST http://api.people.com:3000/people.xml
|
|
||||||
# (and will submit the XML format of the person object in the request)
|
|
||||||
#
|
|
||||||
ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
|
|
||||||
ryan.save #=> true
|
|
||||||
ryan.id #=> 2
|
|
||||||
Person.exists?(ryan.id) #=> true
|
|
||||||
ryan.exists? #=> true
|
|
||||||
|
|
||||||
# Resource creation can also use the convenience <tt>create</tt> method which
|
==== Protocol
|
||||||
# will request a resource save after instantiation.
|
|
||||||
ryan = Person.create(:first => 'Ryan', :last => 'Daigle')
|
|
||||||
ryan.exists? #=> true
|
|
||||||
|
|
||||||
# Updating is done with 'save' as well
|
Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
|
||||||
# PUT http://api.people.com:3000/people/1.xml
|
built into ActionController but will also work with any other REST service that properly implements the protocol.
|
||||||
#
|
REST uses HTTP, but unlike "typical" web applications, it makes use of all the verbs available in the HTTP specification:
|
||||||
ryan = Person.find(1)
|
|
||||||
ryan.first = 'Rizzle'
|
|
||||||
ryan.save #=> true
|
|
||||||
|
|
||||||
# And destruction
|
* GET requests are used for finding and retrieving resources.
|
||||||
# DELETE http://api.people.com:3000/people/1.xml
|
* POST requests are used to create new resources.
|
||||||
#
|
* PUT requests are used to update existing resources.
|
||||||
ryan = Person.find(1)
|
* DELETE requests are used to delete resources.
|
||||||
ryan.destroy #=> true # Or Person.delete(ryan.id)
|
|
||||||
|
|
||||||
|
For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation;
|
||||||
=== Protocol
|
for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer].
|
||||||
|
|
||||||
ARes is built on a standard XML format for requesting and submitting resources. It mirrors the
|
|
||||||
RESTful routing built into ActionController, though it's useful to discuss what ARes expects
|
|
||||||
outside the context of ActionController as it is not dependent on a Rails-based RESTful implementation.
|
|
||||||
|
|
||||||
==== Find
|
==== Find
|
||||||
|
|
||||||
|
@ -169,67 +161,5 @@ Destruction of a resource can be invoked as a class and instance method of the r
|
||||||
Person.exists?(2) #=> false
|
Person.exists?(2) #=> false
|
||||||
|
|
||||||
|
|
||||||
=== Errors & Validation
|
You can find more usage information in the ActiveResource::Base documentation.
|
||||||
|
|
||||||
Error handling and validation is handled in much the same manner as you're used to seeing in
|
|
||||||
ActiveRecord. Both the response code in the Http response and the body of the response are used to
|
|
||||||
indicate that an error occurred.
|
|
||||||
|
|
||||||
==== Resource errors
|
|
||||||
|
|
||||||
When a get is requested for a resource that does not exist, the Http '404' (resource not found)
|
|
||||||
response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
|
|
||||||
exception.
|
|
||||||
|
|
||||||
# GET http://api.people.com:3000/people/1.xml
|
|
||||||
# #=> Response (404)
|
|
||||||
#
|
|
||||||
ryan = Person.find(1) #=> Raises ActiveResource::ResourceNotFound
|
|
||||||
|
|
||||||
==== Validation errors
|
|
||||||
|
|
||||||
Creating and updating resources can lead to validation errors - i.e. 'First name cannot be empty' etc...
|
|
||||||
These types of errors are denoted in the response by a response code of 422 and the xml representation
|
|
||||||
of the validation errors. The save operation will then fail (with a 'false' return value) and the
|
|
||||||
validation errors can be accessed on the resource in question.
|
|
||||||
|
|
||||||
# When
|
|
||||||
#
|
|
||||||
# PUT http://api.people.com:3000/people/1.xml
|
|
||||||
#
|
|
||||||
# is requested with invalid values, the expected response is:
|
|
||||||
#
|
|
||||||
# Response (422):
|
|
||||||
# <errors><error>First cannot be empty</error></errors>
|
|
||||||
#
|
|
||||||
ryan = Person.find(1)
|
|
||||||
ryan.first #=> ''
|
|
||||||
ryan.save #=> false
|
|
||||||
ryan.errors.invalid?(:first) #=> true
|
|
||||||
ryan.errors.full_messages #=> ['First cannot be empty']
|
|
||||||
|
|
||||||
|
|
||||||
==== Response errors
|
|
||||||
|
|
||||||
If the underlying Http request for an ARes operation results in an error response code, an
|
|
||||||
exception will be raised. The following Http response codes will result in these exceptions:
|
|
||||||
|
|
||||||
200 - 399: Valid response, no exception
|
|
||||||
404: ActiveResource::ResourceNotFound
|
|
||||||
409: ActiveResource::ResourceConflict
|
|
||||||
422: ActiveResource::ResourceInvalid (rescued by save as validation errors)
|
|
||||||
401 - 499: ActiveResource::ClientError
|
|
||||||
500 - 599: ActiveResource::ServerError
|
|
||||||
|
|
||||||
|
|
||||||
=== Authentication
|
|
||||||
|
|
||||||
Many REST apis will require username/password authentication, usually in the form of
|
|
||||||
Http authentication. This can easily be specified by putting the username and password
|
|
||||||
in the Url of the ARes site:
|
|
||||||
|
|
||||||
class Person < ActiveResource::Base
|
|
||||||
self.site = "http://ryan:password@api.people.com:3000/"
|
|
||||||
end
|
|
||||||
|
|
||||||
For obvious reasons it is best if such services are available over https.
|
|
||||||
|
|
|
@ -3,12 +3,155 @@ require 'cgi'
|
||||||
require 'set'
|
require 'set'
|
||||||
|
|
||||||
module ActiveResource
|
module ActiveResource
|
||||||
|
# ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
|
||||||
|
#
|
||||||
|
# For an outline of what Active Resource is capable of, see link:files/README.html.
|
||||||
|
#
|
||||||
|
# == Automated mapping
|
||||||
|
#
|
||||||
|
# Active Resource objects represent your RESTful resources as manipulatable Ruby objects. To map resources
|
||||||
|
# to Ruby objects, Active Resource only needs a class name that corresponds to the resource name (e.g., the class
|
||||||
|
# Person maps to the resources people, very similarly to Active Record) and a +site+ value, which holds the
|
||||||
|
# URI of the resources.
|
||||||
|
#
|
||||||
|
# class Person < ActiveResource::Base
|
||||||
|
# self.site = "http://api.people.com:3000/"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# Now the Person class is mapped to RESTful resources located at <tt>http://api.people.com:3000/people/</tt>, and
|
||||||
|
# you can now use Active Resource's lifecycles methods to manipulate resources.
|
||||||
|
#
|
||||||
|
# == Lifecycle methods
|
||||||
|
#
|
||||||
|
# Active Resource exposes methods for creating, finding, updating, and deleting resources
|
||||||
|
# from REST web services.
|
||||||
|
#
|
||||||
|
# ryan = Person.new(:first => 'Ryan', :last => 'Daigle')
|
||||||
|
# ryan.save #=> true
|
||||||
|
# ryan.id #=> 2
|
||||||
|
# Person.exists?(ryan.id) #=> true
|
||||||
|
# ryan.exists? #=> true
|
||||||
|
#
|
||||||
|
# ryan = Person.find(1)
|
||||||
|
# # => Resource holding our newly create Person object
|
||||||
|
#
|
||||||
|
# ryan.first = 'Rizzle'
|
||||||
|
# ryan.save #=> true
|
||||||
|
#
|
||||||
|
# ryan.destroy #=> true
|
||||||
|
#
|
||||||
|
# As you can see, these are very similar to Active Record's lifecycle methods for database records.
|
||||||
|
# You can read more about each of these methods in their respective documentation.
|
||||||
|
#
|
||||||
|
# === Custom REST methods
|
||||||
|
#
|
||||||
|
# Since simple CRUD/lifecycle methods can't accomplish every task, Active Resource also supports
|
||||||
|
# defining your own custom REST methods.
|
||||||
|
#
|
||||||
|
# Person.new(:name => 'Ryan).post(:register)
|
||||||
|
# # => { :id => 1, :name => 'Ryan', :position => 'Clerk' }
|
||||||
|
#
|
||||||
|
# Person.find(1).put(:promote, :position => 'Manager')
|
||||||
|
# # => { :id => 1, :name => 'Ryan', :position => 'Manager' }
|
||||||
|
#
|
||||||
|
# For more information on creating and using custom REST methods, see the
|
||||||
|
# ActiveResource::CustomMethods documentation.
|
||||||
|
#
|
||||||
|
# == Validations
|
||||||
|
#
|
||||||
|
# You can validate resources client side by overriding validation methods in the base class.
|
||||||
|
#
|
||||||
|
# class Person < ActiveResource::Base
|
||||||
|
# self.site = "http://api.people.com:3000/"
|
||||||
|
# protected
|
||||||
|
# def validate
|
||||||
|
# errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# See the ActiveResource::Validations documentation for more information.
|
||||||
|
#
|
||||||
|
# == Authentication
|
||||||
|
#
|
||||||
|
# Many REST APIs will require authentication, usually in the form of basic
|
||||||
|
# HTTP authentication. Authentication can be specified by putting the credentials
|
||||||
|
# in the +site+ variable of the Active Resource class you need to authenticate.
|
||||||
|
#
|
||||||
|
# class Person < ActiveResource::Base
|
||||||
|
# self.site = "http://ryan:password@api.people.com:3000/"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# For obvious security reasons, it is probably best if such services are available
|
||||||
|
# over HTTPS.
|
||||||
|
#
|
||||||
|
# == Errors & Validation
|
||||||
|
#
|
||||||
|
# Error handling and validation is handled in much the same manner as you're used to seeing in
|
||||||
|
# Active Record. Both the response code in the Http response and the body of the response are used to
|
||||||
|
# indicate that an error occurred.
|
||||||
|
#
|
||||||
|
# === Resource errors
|
||||||
|
#
|
||||||
|
# When a get is requested for a resource that does not exist, the HTTP +404+ (Resource Not Found)
|
||||||
|
# response code will be returned from the server which will raise an ActiveResource::ResourceNotFound
|
||||||
|
# exception.
|
||||||
|
#
|
||||||
|
# # GET http://api.people.com:3000/people/999.xml
|
||||||
|
# ryan = Person.find(999) # => Raises ActiveResource::ResourceNotFound
|
||||||
|
# # => Response = 404
|
||||||
|
#
|
||||||
|
# +404+ is just one of the HTTP error response codes that ActiveResource will handle with its own exception. The
|
||||||
|
# following HTTP response codes will also result in these exceptions:
|
||||||
|
#
|
||||||
|
# 200 - 399:: Valid response, no exception
|
||||||
|
# 404:: ActiveResource::ResourceNotFound
|
||||||
|
# 409:: ActiveResource::ResourceConflict
|
||||||
|
# 422:: ActiveResource::ResourceInvalid (rescued by save as validation errors)
|
||||||
|
# 401 - 499:: ActiveResource::ClientError
|
||||||
|
# 500 - 599:: ActiveResource::ServerError
|
||||||
|
#
|
||||||
|
# These custom exceptions allow you to deal with resource errors more naturally and with more precision
|
||||||
|
# rather than returning a general HTTP error. For example:
|
||||||
|
#
|
||||||
|
# begin
|
||||||
|
# ryan = Person.find(my_id)
|
||||||
|
# rescue ActiveResource::ResourceNotFound
|
||||||
|
# redirect_to :action => 'not_found'
|
||||||
|
# rescue ActiveResource::ResourceConflict, ActiveResource::ResourceInvalid
|
||||||
|
# redirect_to :action => 'new'
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# === Validation errors
|
||||||
|
#
|
||||||
|
# Active Resource supports validations on resources and will return errors if any these validations fail
|
||||||
|
# (e.g., "First name can not be blank" and so on). These types of errors are denoted in the response by
|
||||||
|
# a response code of +422+ and an XML representation of the validation errors. The save operation will
|
||||||
|
# then fail (with a +false+ return value) and the validation errors can be accessed on the resource in question.
|
||||||
|
#
|
||||||
|
# ryan = Person.find(1)
|
||||||
|
# ryan.first #=> ''
|
||||||
|
# ryan.save #=> false
|
||||||
|
#
|
||||||
|
# # When
|
||||||
|
# # PUT http://api.people.com:3000/people/1.xml
|
||||||
|
# # is requested with invalid values, the response is:
|
||||||
|
# #
|
||||||
|
# # Response (422):
|
||||||
|
# # <errors><error>First cannot be empty</error></errors>
|
||||||
|
# #
|
||||||
|
#
|
||||||
|
# ryan.errors.invalid?(:first) #=> true
|
||||||
|
# ryan.errors.full_messages #=> ['First cannot be empty']
|
||||||
|
#
|
||||||
|
# Learn more about Active Resource's validation features in the ActiveResource::Validations documentation.
|
||||||
|
#
|
||||||
class Base
|
class Base
|
||||||
# The logger for diagnosing and tracing ARes calls.
|
# The logger for diagnosing and tracing Active Resource calls.
|
||||||
cattr_accessor :logger
|
cattr_accessor :logger
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
# Gets the URI of the resource's site
|
# Gets the URI of the REST resources to map for this class. The site variable is required
|
||||||
|
# ActiveResource's mapping to work.
|
||||||
def site
|
def site
|
||||||
if defined?(@site)
|
if defined?(@site)
|
||||||
@site
|
@site
|
||||||
|
@ -17,13 +160,16 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the URI for the REST resources
|
# Sets the URI of the REST resources to map for this class to the value in the +site+ argument.
|
||||||
|
# The site variable is required ActiveResource's mapping to work.
|
||||||
def site=(site)
|
def site=(site)
|
||||||
@connection = nil
|
@connection = nil
|
||||||
@site = create_site_uri_from(site)
|
@site = create_site_uri_from(site)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Base connection to remote service
|
# An instance of ActiveResource::Connection that is the base connection to the remote service.
|
||||||
|
# The +refresh+ parameter toggles whether or not the connection is refreshed at every request
|
||||||
|
# or not (defaults to +false+).
|
||||||
def connection(refresh = false)
|
def connection(refresh = false)
|
||||||
@connection = Connection.new(site) if refresh || @connection.nil?
|
@connection = Connection.new(site) if refresh || @connection.nil?
|
||||||
@connection
|
@connection
|
||||||
|
@ -40,8 +186,8 @@ module ActiveResource
|
||||||
attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
|
attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
|
||||||
attr_accessor_with_default(:primary_key, 'id') #:nodoc:
|
attr_accessor_with_default(:primary_key, 'id') #:nodoc:
|
||||||
|
|
||||||
# Gets the resource prefix
|
# Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
|
||||||
# prefix/collectionname/1.xml
|
# This method is regenerated at runtime based on what the prefix is set to.
|
||||||
def prefix(options={})
|
def prefix(options={})
|
||||||
default = site.path
|
default = site.path
|
||||||
default << '/' unless default[-1..-1] == '/'
|
default << '/' unless default[-1..-1] == '/'
|
||||||
|
@ -50,13 +196,15 @@ module ActiveResource
|
||||||
prefix(options)
|
prefix(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# An attribute reader for the source string for the resource path prefix. This
|
||||||
|
# method is regenerated at runtime based on what the prefix is set to.
|
||||||
def prefix_source
|
def prefix_source
|
||||||
prefix # generate #prefix and #prefix_source methods first
|
prefix # generate #prefix and #prefix_source methods first
|
||||||
prefix_source
|
prefix_source
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sets the resource prefix
|
# Sets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
|
||||||
# prefix/collectionname/1.xml
|
# Default value is <tt>site.path</tt>.
|
||||||
def prefix=(value = '/')
|
def prefix=(value = '/')
|
||||||
# Replace :placeholders with '#{embedded options[:lookups]}'
|
# Replace :placeholders with '#{embedded options[:lookups]}'
|
||||||
prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
|
prefix_call = value.gsub(/:\w+/) { |key| "\#{options[#{key}]}" }
|
||||||
|
@ -77,23 +225,53 @@ module ActiveResource
|
||||||
alias_method :set_element_name, :element_name= #:nodoc:
|
alias_method :set_element_name, :element_name= #:nodoc:
|
||||||
alias_method :set_collection_name, :collection_name= #:nodoc:
|
alias_method :set_collection_name, :collection_name= #:nodoc:
|
||||||
|
|
||||||
# Gets the element path for the given ID. If no query_options are given, they are split from the prefix options:
|
# Gets the element path for the given ID in +id+. If the +query_options+ parameter is omitted, Rails
|
||||||
|
# will split from the prefix options.
|
||||||
|
#
|
||||||
|
# ==== Options
|
||||||
|
# +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
|
||||||
|
# would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
|
||||||
|
# +query_options+:: A hash to add items to the query string for the request.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Post.element_path(1)
|
||||||
|
# # => /posts/1.xml
|
||||||
|
#
|
||||||
|
# Comment.element_path(1, :post_id => 5)
|
||||||
|
# # => /posts/5/comments/1.xml
|
||||||
|
#
|
||||||
|
# Comment.element_path(1, :post_id => 5, :active => 1)
|
||||||
|
# # => /posts/5/comments/1.xml?active=1
|
||||||
|
#
|
||||||
|
# Comment.element_path(1, {:post_id => 5}, {:active => 1})
|
||||||
|
# # => /posts/5/comments/1.xml?active=1
|
||||||
#
|
#
|
||||||
# Post.element_path(1) # => /posts/1.xml
|
|
||||||
# Comment.element_path(1, :post_id => 5) # => /posts/5/comments/1.xml
|
|
||||||
# Comment.element_path(1, :post_id => 5, :active => 1) # => /posts/5/comments/1.xml?active=1
|
|
||||||
# Comment.element_path(1, {:post_id => 5}, {:active => 1}) # => /posts/5/comments/1.xml?active=1
|
|
||||||
def element_path(id, prefix_options = {}, query_options = nil)
|
def element_path(id, prefix_options = {}, query_options = nil)
|
||||||
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
||||||
"#{prefix(prefix_options)}#{collection_name}/#{id}.xml#{query_string(query_options)}"
|
"#{prefix(prefix_options)}#{collection_name}/#{id}.xml#{query_string(query_options)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gets the collection path. If no query_options are given, they are split from the prefix options:
|
# Gets the collection path for the REST resources. If the +query_options+ parameter is omitted, Rails
|
||||||
|
# will split from the +prefix_options+.
|
||||||
|
#
|
||||||
|
# ==== Options
|
||||||
|
# +prefix_options+:: A hash to add a prefix to the request for nested URL's (e.g., <tt>:account_id => 19</tt>
|
||||||
|
# would yield a URL like <tt>/accounts/19/purchases.xml</tt>).
|
||||||
|
# +query_options+:: A hash to add items to the query string for the request.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Post.collection_path
|
||||||
|
# # => /posts.xml
|
||||||
|
#
|
||||||
|
# Comment.collection_path(:post_id => 5)
|
||||||
|
# # => /posts/5/comments.xml
|
||||||
|
#
|
||||||
|
# Comment.collection_path(:post_id => 5, :active => 1)
|
||||||
|
# # => /posts/5/comments.xml?active=1
|
||||||
|
#
|
||||||
|
# Comment.collection_path({:post_id => 5}, {:active => 1})
|
||||||
|
# # => /posts/5/comments.xml?active=1
|
||||||
#
|
#
|
||||||
# Post.collection_path # => /posts.xml
|
|
||||||
# Comment.collection_path(:post_id => 5) # => /posts/5/comments.xml
|
|
||||||
# Comment.collection_path(:post_id => 5, :active => 1) # => /posts/5/comments.xml?active=1
|
|
||||||
# Comment.collection_path({:post_id => 5}, {:active => 1}) # => /posts/5/comments.xml?active=1
|
|
||||||
def collection_path(prefix_options = {}, query_options = nil)
|
def collection_path(prefix_options = {}, query_options = nil)
|
||||||
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
|
||||||
"#{prefix(prefix_options)}#{collection_name}.xml#{query_string(query_options)}"
|
"#{prefix(prefix_options)}#{collection_name}.xml#{query_string(query_options)}"
|
||||||
|
@ -102,30 +280,77 @@ module ActiveResource
|
||||||
alias_method :set_primary_key, :primary_key= #:nodoc:
|
alias_method :set_primary_key, :primary_key= #:nodoc:
|
||||||
|
|
||||||
# Create a new resource instance and request to the remote service
|
# Create a new resource instance and request to the remote service
|
||||||
# that it be saved. This is equivalent to the following simultaneous calls:
|
# that it be saved, making it equivalent to the following simultaneous calls:
|
||||||
#
|
#
|
||||||
# ryan = Person.new(:first => 'ryan')
|
# ryan = Person.new(:first => 'ryan')
|
||||||
# ryan.save
|
# ryan.save
|
||||||
#
|
#
|
||||||
# The newly created resource is returned. If a failure has occurred an
|
# The newly created resource is returned. If a failure has occurred an
|
||||||
# exception will be raised (see save). If the resource is invalid and
|
# exception will be raised (see save). If the resource is invalid and
|
||||||
# has not been saved then <tt>resource.valid?</tt> will return <tt>false</tt>,
|
# has not been saved then valid? will return <tt>false</tt>,
|
||||||
# while <tt>resource.new?</tt> will still return <tt>true</tt>.
|
# while new? will still return <tt>true</tt>.
|
||||||
#
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Person.create(:name => 'Jeremy', :email => 'myname@nospam.com', :enabled => true)
|
||||||
|
# my_person = Person.find(:first)
|
||||||
|
# my_person.email
|
||||||
|
# # => myname@nospam.com
|
||||||
|
#
|
||||||
|
# dhh = Person.create(:name => 'David', :email => 'dhh@nospam.com', :enabled => true)
|
||||||
|
# dhh.valid?
|
||||||
|
# # => true
|
||||||
|
# dhh.new?
|
||||||
|
# # => false
|
||||||
|
#
|
||||||
|
# # We'll assume that there's a validation that requires the name attribute
|
||||||
|
# that_guy = Person.create(:name => '', :email => 'thatguy@nospam.com', :enabled => true)
|
||||||
|
# that_guy.valid?
|
||||||
|
# # => false
|
||||||
|
# that_guy.new?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
def create(attributes = {})
|
def create(attributes = {})
|
||||||
returning(self.new(attributes)) { |res| res.save }
|
returning(self.new(attributes)) { |res| res.save }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Core method for finding resources. Used similarly to Active Record's find method.
|
# Core method for finding resources. Used similarly to Active Record's find method.
|
||||||
#
|
#
|
||||||
# Person.find(1) # => GET /people/1.xml
|
# ==== Arguments
|
||||||
# Person.find(:all) # => GET /people.xml
|
# The first argument is considered to be the scope of the query. That is, how many
|
||||||
# Person.find(:all, :params => { :title => "CEO" }) # => GET /people.xml?title=CEO
|
# resources are returned from the request. It can be one of the following.
|
||||||
# Person.find(:all, :from => :managers) # => GET /people/managers.xml
|
#
|
||||||
# Person.find(:all, :from => "/companies/1/people.xml") # => GET /companies/1/people.xml
|
# +:one+:: Returns a single resource.
|
||||||
# Person.find(:one, :from => :leader) # => GET /people/leader.xml
|
# +:first+:: Returns the first resource found.
|
||||||
# Person.find(:one, :from => "/companies/1/manager.xml") # => GET /companies/1/manager.xml
|
# +:all+:: Returns every resource that matches the request.
|
||||||
# StreetAddress.find(1, :params => { :person_id => 1 }) # => GET /people/1/street_addresses/1.xml
|
#
|
||||||
|
# ==== Options
|
||||||
|
# +from+:: Sets the path or custom method that resources will be fetched from.
|
||||||
|
# +params+:: Sets query and prefix (nested URL) parameters.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Person.find(1)
|
||||||
|
# # => GET /people/1.xml
|
||||||
|
#
|
||||||
|
# Person.find(:all)
|
||||||
|
# # => GET /people.xml
|
||||||
|
#
|
||||||
|
# Person.find(:all, :params => { :title => "CEO" })
|
||||||
|
# # => GET /people.xml?title=CEO
|
||||||
|
#
|
||||||
|
# Person.find(:first, :from => :managers)
|
||||||
|
# # => GET /people/managers.xml
|
||||||
|
#
|
||||||
|
# Person.find(:all, :from => "/companies/1/people.xml")
|
||||||
|
# # => GET /companies/1/people.xml
|
||||||
|
#
|
||||||
|
# Person.find(:one, :from => :leader)
|
||||||
|
# # => GET /people/leader.xml
|
||||||
|
#
|
||||||
|
# Person.find(:one, :from => "/companies/1/manager.xml")
|
||||||
|
# # => GET /companies/1/manager.xml
|
||||||
|
#
|
||||||
|
# StreetAddress.find(1, :params => { :person_id => 1 })
|
||||||
|
# # => GET /people/1/street_addresses/1.xml
|
||||||
def find(*arguments)
|
def find(*arguments)
|
||||||
scope = arguments.slice!(0)
|
scope = arguments.slice!(0)
|
||||||
options = arguments.slice!(0) || {}
|
options = arguments.slice!(0) || {}
|
||||||
|
@ -138,11 +363,38 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Deletes the resources with the ID in the +id+ parameter.
|
||||||
|
#
|
||||||
|
# ==== Options
|
||||||
|
# All options specify prefix and query parameters.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Event.delete(2)
|
||||||
|
# # => DELETE /events/2
|
||||||
|
#
|
||||||
|
# Event.create(:name => 'Free Concert', :location => 'Community Center')
|
||||||
|
# my_event = Event.find(:first)
|
||||||
|
# # => Events (id: 7)
|
||||||
|
# Event.delete(my_event.id)
|
||||||
|
# # => DELETE /events/7
|
||||||
|
#
|
||||||
|
# # Let's assume a request to events/5/cancel.xml
|
||||||
|
# Event.delete(params[:id])
|
||||||
|
# # => DELETE /events/5
|
||||||
|
#
|
||||||
def delete(id, options = {})
|
def delete(id, options = {})
|
||||||
connection.delete(element_path(id, options))
|
connection.delete(element_path(id, options))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Evalutes to <tt>true</tt> if the resource is found.
|
# Asserts the existence of a resource, returning <tt>true</tt> if the resource is found.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Note.create(:title => 'Hello, world.', :body => 'Nothing more for now...')
|
||||||
|
# Note.exists?(1)
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# Note.exists(1349)
|
||||||
|
# # => false
|
||||||
def exists?(id, options = {})
|
def exists?(id, options = {})
|
||||||
id && !find_single(id, options).nil?
|
id && !find_single(id, options).nil?
|
||||||
rescue ActiveResource::ResourceNotFound
|
rescue ActiveResource::ResourceNotFound
|
||||||
|
@ -226,33 +478,79 @@ module ActiveResource
|
||||||
attr_accessor :attributes #:nodoc:
|
attr_accessor :attributes #:nodoc:
|
||||||
attr_accessor :prefix_options #:nodoc:
|
attr_accessor :prefix_options #:nodoc:
|
||||||
|
|
||||||
|
# Constructor method for new resources; the optional +attributes+ parameter takes a +Hash+
|
||||||
|
# of attributes for the new resource.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_course = Course.new
|
||||||
|
# my_course.name = "Western Civilization"
|
||||||
|
# my_course.lecturer = "Don Trotter"
|
||||||
|
# my_course.save
|
||||||
|
#
|
||||||
|
# my_other_course = Course.new(:name => "Philosophy: Reason and Being", :lecturer => "Ralph Cling")
|
||||||
|
# my_other_course.save
|
||||||
def initialize(attributes = {})
|
def initialize(attributes = {})
|
||||||
@attributes = {}
|
@attributes = {}
|
||||||
@prefix_options = {}
|
@prefix_options = {}
|
||||||
load(attributes)
|
load(attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Is the resource a new object?
|
# A method to determine if the resource a new object (i.e., it has not been POSTed to the remote service yet).
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# not_new = Computer.create(:brand => 'Apple', :make => 'MacBook', :vendor => 'MacMall')
|
||||||
|
# not_new.new?
|
||||||
|
# # => false
|
||||||
|
#
|
||||||
|
# is_new = Computer.new(:brand => 'IBM', :make => 'Thinkpad', :vendor => 'IBM')
|
||||||
|
# is_new.new?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# is_new.save
|
||||||
|
# is_new.new?
|
||||||
|
# # => false
|
||||||
|
#
|
||||||
def new?
|
def new?
|
||||||
id.nil?
|
id.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get the id of the object.
|
# Get the +id+ attribute of the resource.
|
||||||
def id
|
def id
|
||||||
attributes[self.class.primary_key]
|
attributes[self.class.primary_key]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the id of the object.
|
# Set the +id+ attribute of the resource.
|
||||||
def id=(id)
|
def id=(id)
|
||||||
attributes[self.class.primary_key] = id
|
attributes[self.class.primary_key] = id
|
||||||
end
|
end
|
||||||
|
|
||||||
# True if and only if +other+ is the same object or is an instance of the same class, is not +new?+, and has the same +id+.
|
# Test for equality. Resource are equal if and only if +other+ is the same object or
|
||||||
|
# is an instance of the same class, is not +new?+, and has the same +id+.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# ryan = Person.create(:name => 'Ryan')
|
||||||
|
# jamie = Person.create(:name => 'Jamie')
|
||||||
|
#
|
||||||
|
# ryan == jamie
|
||||||
|
# # => false (Different name attribute and id)
|
||||||
|
#
|
||||||
|
# ryan_again = Person.new(:name => 'Ryan')
|
||||||
|
# ryan == ryan_again
|
||||||
|
# # => false (ryan_again is new?)
|
||||||
|
#
|
||||||
|
# ryans_clone = Person.create(:name => 'Ryan')
|
||||||
|
# ryan == ryans_clone
|
||||||
|
# # => false (Different id attributes)
|
||||||
|
#
|
||||||
|
# ryans_twin = Person.find(ryan.id)
|
||||||
|
# ryan == ryans_twin
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
def ==(other)
|
def ==(other)
|
||||||
other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
|
other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delegates to ==
|
# Tests for equality (delegates to ==).
|
||||||
def eql?(other)
|
def eql?(other)
|
||||||
self == other
|
self == other
|
||||||
end
|
end
|
||||||
|
@ -263,6 +561,22 @@ module ActiveResource
|
||||||
id.hash
|
id.hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Duplicate the current resource without saving it.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_invoice = Invoice.create(:customer => 'That Company')
|
||||||
|
# next_invoice = my_invoice.dup
|
||||||
|
# next_invoice.new?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# next_invoice.save
|
||||||
|
# next_invoice == my_invoice
|
||||||
|
# # => false (different id attributes)
|
||||||
|
#
|
||||||
|
# my_invoice.customer
|
||||||
|
# # => That Company
|
||||||
|
# next_invoice.customer
|
||||||
|
# # => That Company
|
||||||
def dup
|
def dup
|
||||||
returning new do |resource|
|
returning new do |resource|
|
||||||
resource.attributes = @attributes
|
resource.attributes = @attributes
|
||||||
|
@ -270,35 +584,137 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delegates to +create+ if a new object, +update+ if its old. If the response to the save includes a body,
|
# A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object,
|
||||||
# it will be assumed that this body is XML for the final object as it looked after the save (which would include
|
# +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body
|
||||||
# attributes like created_at that wasn't part of the original submit).
|
# is XML for the final object as it looked after the save (which would include attributes like +created_at+
|
||||||
|
# that weren't part of the original submit).
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_company = Company.new(:name => 'RoleModel Software', :owner => 'Ken Auer', :size => 2)
|
||||||
|
# my_company.new?
|
||||||
|
# # => true
|
||||||
|
# my_company.save
|
||||||
|
# # => POST /companies/ (create)
|
||||||
|
#
|
||||||
|
# my_company.new?
|
||||||
|
# # => false
|
||||||
|
# my_company.size = 10
|
||||||
|
# my_company.save
|
||||||
|
# # => PUT /companies/1 (update)
|
||||||
def save
|
def save
|
||||||
new? ? create : update
|
new? ? create : update
|
||||||
end
|
end
|
||||||
|
|
||||||
# Delete the resource.
|
# Deletes the resource from the remote service.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_id = 3
|
||||||
|
# my_person = Person.find(my_id)
|
||||||
|
# my_person.destroy
|
||||||
|
# Person.find(my_id)
|
||||||
|
# # => 404 (Resource Not Found)
|
||||||
|
#
|
||||||
|
# new_person = Person.create(:name => 'James')
|
||||||
|
# new_id = new_person.id
|
||||||
|
# # => 7
|
||||||
|
# new_person.destroy
|
||||||
|
# Person.find(new_id)
|
||||||
|
# # => 404 (Resource Not Found)
|
||||||
def destroy
|
def destroy
|
||||||
connection.delete(element_path, self.class.headers)
|
connection.delete(element_path, self.class.headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Evaluates to <tt>true</tt> if this resource is found.
|
# Evaluates to <tt>true</tt> if this resource is not +new?+ and is
|
||||||
|
# found on the remote service. Using this method, you can check for
|
||||||
|
# resources that may have been deleted between the object's instantiation
|
||||||
|
# and actions on it.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# Person.create(:name => 'Theodore Roosevelt')
|
||||||
|
# that_guy = Person.find(:first)
|
||||||
|
# that_guy.exists?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# that_lady = Person.new(:name => 'Paul Bean')
|
||||||
|
# that_lady.exists?
|
||||||
|
# # => false
|
||||||
|
#
|
||||||
|
# guys_id = that_guy.id
|
||||||
|
# Person.delete(guys_id)
|
||||||
|
# that_guy.exists?
|
||||||
|
# # => false
|
||||||
def exists?
|
def exists?
|
||||||
!new? && self.class.exists?(id, :params => prefix_options)
|
!new? && self.class.exists?(id, :params => prefix_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convert the resource to an XML string
|
# A method to convert the the resource to an XML string.
|
||||||
|
#
|
||||||
|
# ==== Options
|
||||||
|
# The +options+ parameter is handed off to the +to_xml+ method on each
|
||||||
|
# attribute, so it has the same options as the +to_xml+ methods in
|
||||||
|
# ActiveSupport.
|
||||||
|
#
|
||||||
|
# indent:: Set the indent level for the XML output (default is +2+).
|
||||||
|
# dasherize:: Boolean option to determine whether or not element names should
|
||||||
|
# replace underscores with dashes (default is +false+).
|
||||||
|
# skip_instruct:: Toggle skipping the +instruct!+ call on the XML builder
|
||||||
|
# that generates the XML declaration (default is +false+).
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_group = SubsidiaryGroup.find(:first)
|
||||||
|
# my_group.to_xml
|
||||||
|
# # => <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
# # <subsidiary_group> [...] </subsidiary_group>
|
||||||
|
#
|
||||||
|
# my_group.to_xml(:dasherize => true)
|
||||||
|
# # => <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
# # <subsidiary-group> [...] </subsidiary-group>
|
||||||
|
#
|
||||||
|
# my_group.to_xml(:skip_instruct => true)
|
||||||
|
# # => <subsidiary_group> [...] </subsidiary_group>
|
||||||
def to_xml(options={})
|
def to_xml(options={})
|
||||||
attributes.to_xml({:root => self.class.element_name}.merge(options))
|
attributes.to_xml({:root => self.class.element_name}.merge(options))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reloads the attributes of this object from the remote web service.
|
# A method to reload the attributes of this object from the remote web service.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_branch = Branch.find(:first)
|
||||||
|
# my_branch.name
|
||||||
|
# # => Wislon Raod
|
||||||
|
#
|
||||||
|
# # Another client fixes the typo...
|
||||||
|
#
|
||||||
|
# my_branch.name
|
||||||
|
# # => Wislon Raod
|
||||||
|
# my_branch.reload
|
||||||
|
# my_branch.name
|
||||||
|
# # => Wilson Road
|
||||||
def reload
|
def reload
|
||||||
self.load(self.class.find(id, :params => @prefix_options).attributes)
|
self.load(self.class.find(id, :params => @prefix_options).attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Manually load attributes from a hash. Recursively loads collections of
|
# A method to manually load attributes from a hash. Recursively loads collections of
|
||||||
# resources.
|
# resources. This method is called in initialize and create when a +Hash+ of attributes
|
||||||
|
# is provided.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_attrs = {:name => 'J&J Textiles', :industry => 'Cloth and textiles'}
|
||||||
|
#
|
||||||
|
# the_supplier = Supplier.find(:first)
|
||||||
|
# the_supplier.name
|
||||||
|
# # => 'J&M Textiles'
|
||||||
|
# the_supplier.load(my_attrs)
|
||||||
|
# the_supplier.name('J&J Textiles')
|
||||||
|
#
|
||||||
|
# # These two calls are the same as Supplier.new(my_attrs)
|
||||||
|
# my_supplier = Supplier.new
|
||||||
|
# my_supplier.load(my_attrs)
|
||||||
|
#
|
||||||
|
# # These three calls are the same as Supplier.create(my_attrs)
|
||||||
|
# your_supplier = Supplier.new
|
||||||
|
# your_supplier.load(my_attrs)
|
||||||
|
# your_supplier.save
|
||||||
def load(attributes)
|
def load(attributes)
|
||||||
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
||||||
@prefix_options, attributes = split_options(attributes)
|
@prefix_options, attributes = split_options(attributes)
|
||||||
|
@ -321,8 +737,9 @@ module ActiveResource
|
||||||
# For checking respond_to? without searching the attributes (which is faster).
|
# For checking respond_to? without searching the attributes (which is faster).
|
||||||
alias_method :respond_to_without_attributes?, :respond_to?
|
alias_method :respond_to_without_attributes?, :respond_to?
|
||||||
|
|
||||||
# A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
|
# A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a +Person+ object with a
|
||||||
# person.respond_to?("name?") which will all return true.
|
# +name+ attribute can answer +true+ to +my_person.respond_to?("name")+, +my_person.respond_to?("name=")+, and
|
||||||
|
# +my_person.respond_to?("name?")+.
|
||||||
def respond_to?(method, include_priv = false)
|
def respond_to?(method, include_priv = false)
|
||||||
method_name = method.to_s
|
method_name = method.to_s
|
||||||
if attributes.nil?
|
if attributes.nil?
|
||||||
|
|
|
@ -5,7 +5,7 @@ require 'uri'
|
||||||
require 'benchmark'
|
require 'benchmark'
|
||||||
|
|
||||||
module ActiveResource
|
module ActiveResource
|
||||||
class ConnectionError < StandardError
|
class ConnectionError < StandardError # :nodoc:
|
||||||
attr_reader :response
|
attr_reader :response
|
||||||
|
|
||||||
def initialize(response, message = nil)
|
def initialize(response, message = nil)
|
||||||
|
@ -18,20 +18,28 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ClientError < ConnectionError; end # 4xx Client Error
|
# 4xx Client Error
|
||||||
class ResourceNotFound < ClientError; end # 404 Not Found
|
class ClientError < ConnectionError; end # :nodoc:
|
||||||
class ResourceConflict < ClientError; end # 409 Conflict
|
|
||||||
|
# 404 Not Found
|
||||||
|
class ResourceNotFound < ClientError; end # :nodoc:
|
||||||
|
|
||||||
|
# 409 Conflict
|
||||||
|
class ResourceConflict < ClientError; end # :nodoc:
|
||||||
|
|
||||||
class ServerError < ConnectionError; end # 5xx Server Error
|
# 5xx Server Error
|
||||||
|
class ServerError < ConnectionError; end # :nodoc:
|
||||||
|
|
||||||
# 405 Method Not Allowed
|
# 405 Method Not Allowed
|
||||||
class MethodNotAllowed < ClientError
|
class MethodNotAllowed < ClientError # :nodoc:
|
||||||
def allowed_methods
|
def allowed_methods
|
||||||
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Class to handle connections to remote services.
|
# Class to handle connections to remote web services.
|
||||||
|
# This class is used by ActiveResource::Base to interface with REST
|
||||||
|
# services.
|
||||||
class Connection
|
class Connection
|
||||||
attr_reader :site
|
attr_reader :site
|
||||||
|
|
||||||
|
@ -46,6 +54,8 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The +site+ parameter is required and will set the +site+
|
||||||
|
# attribute to the URI for the remote resource service.
|
||||||
def initialize(site)
|
def initialize(site)
|
||||||
raise ArgumentError, 'Missing site URI' unless site
|
raise ArgumentError, 'Missing site URI' unless site
|
||||||
self.site = site
|
self.site = site
|
||||||
|
@ -84,7 +94,6 @@ module ActiveResource
|
||||||
from_xml_data(Hash.from_xml(response.body))
|
from_xml_data(Hash.from_xml(response.body))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
# Makes request to remote service.
|
# Makes request to remote service.
|
||||||
def request(method, path, *arguments)
|
def request(method, path, *arguments)
|
||||||
|
@ -152,6 +161,5 @@ module ActiveResource
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,31 +1,34 @@
|
||||||
# Support custom methods and sub-resources for REST.
|
# A module to support custom REST methods and sub-resources, allowing you to break out
|
||||||
|
# of the "default" REST methods with your own custom resource requests. For example,
|
||||||
|
# say you use Rails to expose a REST service and configure your routes with:
|
||||||
#
|
#
|
||||||
# Say you on the server configure your routes with:
|
# map.resources :people, :new => { :register => :post },
|
||||||
|
# :element => { :promote => :put, :deactivate => :delete }
|
||||||
|
# :collection => { :active => :get }
|
||||||
#
|
#
|
||||||
# map.resources :people, :new => { :register => :post },
|
# This route set creates routes for the following http requests:
|
||||||
# :element => { :promote => :put, :deactivate => :delete }
|
|
||||||
# :collection => { :active => :get }
|
|
||||||
#
|
#
|
||||||
# Which creates routes for the following http requests:
|
# POST /people/new/register.xml #=> PeopleController.register
|
||||||
|
# PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1
|
||||||
|
# DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1
|
||||||
|
# GET /people/active.xml #=> PeopleController.active
|
||||||
#
|
#
|
||||||
# POST /people/new/register.xml #=> PeopleController.register
|
# Using this module, Active Resource can use these custom REST methods just like the
|
||||||
# PUT /people/1/promote.xml #=> PeopleController.promote with :id => 1
|
# standard methods.
|
||||||
# DELETE /people/1/deactivate.xml #=> PeopleController.deactivate with :id => 1
|
|
||||||
# GET /people/active.xml #=> PeopleController.active
|
|
||||||
#
|
|
||||||
# This module provides the ability for Active Resource to call these
|
|
||||||
# custom REST methods and get the response back.
|
|
||||||
#
|
#
|
||||||
# class Person < ActiveResource::Base
|
# class Person < ActiveResource::Base
|
||||||
# self.site = "http://37s.sunrise.i:3000"
|
# self.site = "http://37s.sunrise.i:3000"
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# Person.new(:name => 'Ryan).post(:register) #=> { :id => 1, :name => 'Ryan' }
|
# Person.new(:name => 'Ryan).post(:register) # POST /people/new/register.xml
|
||||||
|
# # => { :id => 1, :name => 'Ryan' }
|
||||||
#
|
#
|
||||||
# Person.find(1).put(:promote, :position => 'Manager')
|
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
|
||||||
# Person.find(1).delete(:deactivate)
|
# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
|
||||||
|
#
|
||||||
|
# Person.get(:active) # GET /people/active.xml
|
||||||
|
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
||||||
#
|
#
|
||||||
# Person.get(:active) #=> [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
|
|
||||||
module ActiveResource
|
module ActiveResource
|
||||||
module CustomMethods
|
module CustomMethods
|
||||||
def self.included(within)
|
def self.included(within)
|
||||||
|
|
|
@ -14,23 +14,76 @@ module ActiveResource
|
||||||
@base, @errors = base, {}
|
@base, @errors = base, {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Add an error to the base Active Resource object rather than an attribute.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_folder = Folder.find(1)
|
||||||
|
# my_folder.errors.add_to_base("You can't edit an existing folder")
|
||||||
|
# my_folder.errors.on_base
|
||||||
|
# # => "You can't edit an existing folder"
|
||||||
|
#
|
||||||
|
# my_folder.errors.add_to_base("This folder has been tagged as frozen")
|
||||||
|
# my_folder.valid?
|
||||||
|
# # => false
|
||||||
|
# my_folder.errors.on_base
|
||||||
|
# # => ["You can't edit an existing folder", "This folder has been tagged as frozen"]
|
||||||
|
#
|
||||||
def add_to_base(msg)
|
def add_to_base(msg)
|
||||||
add(:base, msg)
|
add(:base, msg)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter)
|
||||||
|
# with the error message in +msg+.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_resource = Node.find(1)
|
||||||
|
# my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base'
|
||||||
|
# my_resource.errors.on('name')
|
||||||
|
# # => 'can not be "base"!'
|
||||||
|
#
|
||||||
|
# my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == ''
|
||||||
|
# my_resource.valid?
|
||||||
|
# # => false
|
||||||
|
# my_resource.errors.on('desc')
|
||||||
|
# # => 'can not be blank!'
|
||||||
|
#
|
||||||
def add(attribute, msg)
|
def add(attribute, msg)
|
||||||
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
|
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
|
||||||
@errors[attribute.to_s] << msg
|
@errors[attribute.to_s] << msg
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if the specified +attribute+ has errors associated with it.
|
# Returns true if the specified +attribute+ has errors associated with it.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_resource = Disk.find(1)
|
||||||
|
# my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main'
|
||||||
|
# my_resource.errors.on('location')
|
||||||
|
# # => 'must be Main!'
|
||||||
|
#
|
||||||
|
# my_resource.errors.invalid?('location')
|
||||||
|
# # => true
|
||||||
|
# my_resource.errors.invalid?('name')
|
||||||
|
# # => false
|
||||||
def invalid?(attribute)
|
def invalid?(attribute)
|
||||||
!@errors[attribute.to_s].nil?
|
!@errors[attribute.to_s].nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# * Returns nil, if no errors are associated with the specified +attribute+.
|
# A method to return the errors associated with +attribute+, which returns nil, if no errors are
|
||||||
# * Returns the error message, if one error is associated with the specified +attribute+.
|
# associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
|
||||||
# * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
|
# or an array of error messages if more than one error is associated with the specified +attribute+.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_person = Person.new(params[:person])
|
||||||
|
# my_person.errors.on('login')
|
||||||
|
# # => nil
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||||
|
# my_person.errors.on('login')
|
||||||
|
# # => 'can not be empty'
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10
|
||||||
|
# my_person.errors.on('login')
|
||||||
|
# # => ['can not be empty', 'can not be longer than 10 characters']
|
||||||
def on(attribute)
|
def on(attribute)
|
||||||
errors = @errors[attribute.to_s]
|
errors = @errors[attribute.to_s]
|
||||||
return nil if errors.nil?
|
return nil if errors.nil?
|
||||||
|
@ -39,23 +92,72 @@ module ActiveResource
|
||||||
|
|
||||||
alias :[] :on
|
alias :[] :on
|
||||||
|
|
||||||
# Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
|
# A method to return errors assigned to +base+ object through add_to_base, which returns nil, if no errors are
|
||||||
|
# associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
|
||||||
|
# or an array of error messages if more than one error is associated with the specified +attribute+.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_account = Account.find(1)
|
||||||
|
# my_account.errors.on_base
|
||||||
|
# # => nil
|
||||||
|
#
|
||||||
|
# my_account.errors.add_to_base("This account is frozen")
|
||||||
|
# my_account.errors.on_base
|
||||||
|
# # => "This account is frozen"
|
||||||
|
#
|
||||||
|
# my_account.errors.add_to_base("This account has been closed")
|
||||||
|
# my_account.errors.on_base
|
||||||
|
# # => ["This account is frozen", "This account has been closed"]
|
||||||
|
#
|
||||||
def on_base
|
def on_base
|
||||||
on(:base)
|
on(:base)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Yields each attribute and associated message per error added.
|
# Yields each attribute and associated message per error added.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_person = Person.new(params[:person])
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||||
|
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||||
|
# messages = ''
|
||||||
|
# my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"}
|
||||||
|
# messages
|
||||||
|
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||||
|
#
|
||||||
def each
|
def each
|
||||||
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
|
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
|
||||||
# through iteration as "First name can't be empty".
|
# through iteration as "First name can't be empty".
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_person = Person.new(params[:person])
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||||
|
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||||
|
# messages = ''
|
||||||
|
# my_person.errors.each_full {|msg| messages += msg + "<br/>"}
|
||||||
|
# messages
|
||||||
|
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||||
|
#
|
||||||
def each_full
|
def each_full
|
||||||
full_messages.each { |msg| yield msg }
|
full_messages.each { |msg| yield msg }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns all the full error messages in an array.
|
# Returns all the full error messages in an array.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_person = Person.new(params[:person])
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||||
|
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||||
|
# messages = ''
|
||||||
|
# my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"}
|
||||||
|
# messages
|
||||||
|
# # => "Login can not be empty<br />Password can not be empty<br />"
|
||||||
|
#
|
||||||
def full_messages
|
def full_messages
|
||||||
full_messages = []
|
full_messages = []
|
||||||
|
|
||||||
|
@ -79,6 +181,17 @@ module ActiveResource
|
||||||
|
|
||||||
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
|
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
|
||||||
# with this as well.
|
# with this as well.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_person = Person.new(params[:person])
|
||||||
|
# my_person.errors.size
|
||||||
|
# # => 0
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||||
|
# my_person.errors.add('password', 'can not be empty') if my_person.password == ''
|
||||||
|
# my_person.error.size
|
||||||
|
# # => 2
|
||||||
|
#
|
||||||
def size
|
def size
|
||||||
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
||||||
end
|
end
|
||||||
|
@ -86,6 +199,7 @@ module ActiveResource
|
||||||
alias_method :count, :size
|
alias_method :count, :size
|
||||||
alias_method :length, :size
|
alias_method :length, :size
|
||||||
|
|
||||||
|
# Grabs errors from the XML response.
|
||||||
def from_xml(xml)
|
def from_xml(xml)
|
||||||
clear
|
clear
|
||||||
humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
|
humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
|
||||||
|
@ -102,9 +216,12 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Module to allow validation of ActiveResource objects, which are implemented by overriding +Base#validate+ or its variants.
|
# Module to allow validation of ActiveResource objects, which creates an Errors instance for every resource.
|
||||||
# Each of these methods can inspect the state of the object, which usually means ensuring that a number of
|
# Methods are implemented by overriding +Base#validate+ or its variants Each of these methods can inspect
|
||||||
# attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). For example:
|
# the state of the object, which usually means ensuring that a number of attributes have a certain value
|
||||||
|
# (such as not empty, within a given range, matching a certain regular expression and so on).
|
||||||
|
#
|
||||||
|
# ==== Example
|
||||||
#
|
#
|
||||||
# class Person < ActiveResource::Base
|
# class Person < ActiveResource::Base
|
||||||
# self.site = "http://www.localhost.com:3000/"
|
# self.site = "http://www.localhost.com:3000/"
|
||||||
|
@ -133,7 +250,6 @@ module ActiveResource
|
||||||
# person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
|
# person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
|
||||||
# person.save # => true (and person is now saved to the remote service)
|
# person.save # => true (and person is now saved to the remote service)
|
||||||
#
|
#
|
||||||
# An Errors object is automatically created for every resource.
|
|
||||||
module Validations
|
module Validations
|
||||||
def self.included(base) # :nodoc:
|
def self.included(base) # :nodoc:
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
|
@ -141,6 +257,7 @@ module ActiveResource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Validate a resource and save (POST) it to the remote web service.
|
||||||
def save_with_validation
|
def save_with_validation
|
||||||
save_without_validation
|
save_without_validation
|
||||||
true
|
true
|
||||||
|
@ -149,6 +266,16 @@ module ActiveResource
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Checks for errors on an object (i.e., is resource.errors empty?).
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
# my_person = Person.create(params[:person])
|
||||||
|
# my_person.valid?
|
||||||
|
# # => true
|
||||||
|
#
|
||||||
|
# my_person.errors.add('login', 'can not be empty') if my_person.login == ''
|
||||||
|
# my_person.valid?
|
||||||
|
# # => false
|
||||||
def valid?
|
def valid?
|
||||||
errors.empty?
|
errors.empty?
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue