1
0
Fork 0
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:
David Heinemeier Hansson 2007-06-23 17:29:54 +00:00
parent 753cbf1cd4
commit ae4838fff2
5 changed files with 674 additions and 189 deletions

View file

@ -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
intended to provide transparent proxying capabilities between a client and a RESTful
service (for which Rails provides the {Simply RESTful routing}[http://dev.rubyonrails.org/browser/trunk/actionpack/lib/action_controller/resources.rb] implementation).
Active Resource (ARes) connects business objects and Representational State Transfer (REST)
web services. It implements object-relational mapping for REST webservices to provide transparent
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
class variable:
Active Resource attempts to provide a coherent wrapper object-relational mapping for REST
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
self.site = "http://api.people.com:3000/"
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.
# 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)
Person.exists?(1) #=> true
# To create a new person - instantiate the object and call 'save',
# which will invoke this Http call:
# 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
As you can see, the methods are quite similar to Active Record's methods for dealing with database
records. But rather than dealing with
# Resource creation can also use the convenience <tt>create</tt> method which
# will request a resource save after instantiation.
ryan = Person.create(:first => 'Ryan', :last => 'Daigle')
ryan.exists? #=> true
==== Protocol
# Updating is done with 'save' as well
# PUT http://api.people.com:3000/people/1.xml
#
ryan = Person.find(1)
ryan.first = 'Rizzle'
ryan.save #=> true
Active Resource is built on a standard XML format for requesting and submitting resources over HTTP. It mirrors the RESTful routing
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:
# And destruction
# DELETE http://api.people.com:3000/people/1.xml
#
ryan = Person.find(1)
ryan.destroy #=> true # Or Person.delete(ryan.id)
* GET requests are used for finding and retrieving resources.
* POST requests are used to create new resources.
* PUT requests are used to update existing resources.
* DELETE requests are used to delete resources.
=== Protocol
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.
For more information on how this protocol works with Active Resource, see the ActiveResource::Base documentation;
for more general information on REST web services, see the article here[http://en.wikipedia.org/wiki/Representational_State_Transfer].
==== 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
=== 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.

View file

@ -3,12 +3,155 @@ require 'cgi'
require 'set'
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
# The logger for diagnosing and tracing ARes calls.
# The logger for diagnosing and tracing Active Resource calls.
cattr_accessor :logger
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
if defined?(@site)
@site
@ -17,13 +160,16 @@ module ActiveResource
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)
@connection = nil
@site = create_site_uri_from(site)
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)
@connection = Connection.new(site) if refresh || @connection.nil?
@connection
@ -40,8 +186,8 @@ module ActiveResource
attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
attr_accessor_with_default(:primary_key, 'id') #:nodoc:
# Gets the resource prefix
# prefix/collectionname/1.xml
# Gets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>)
# This method is regenerated at runtime based on what the prefix is set to.
def prefix(options={})
default = site.path
default << '/' unless default[-1..-1] == '/'
@ -50,13 +196,15 @@ module ActiveResource
prefix(options)
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
prefix # generate #prefix and #prefix_source methods first
prefix_source
end
# Sets the resource prefix
# prefix/collectionname/1.xml
# Sets the prefix for a resource's nested URL (e.g., <tt>prefix/collectionname/1.xml</tt>).
# Default value is <tt>site.path</tt>.
def prefix=(value = '/')
# Replace :placeholders with '#{embedded options[:lookups]}'
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_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)
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
"#{prefix(prefix_options)}#{collection_name}/#{id}.xml#{query_string(query_options)}"
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)
prefix_options, query_options = split_options(prefix_options) if query_options.nil?
"#{prefix(prefix_options)}#{collection_name}.xml#{query_string(query_options)}"
@ -102,15 +280,34 @@ module ActiveResource
alias_method :set_primary_key, :primary_key= #:nodoc:
# 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.save
#
# The newly created resource is returned. If a failure has occurred an
# 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>,
# while <tt>resource.new?</tt> will still return <tt>true</tt>.
# has not been saved then valid? will return <tt>false</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 = {})
returning(self.new(attributes)) { |res| res.save }
@ -118,14 +315,42 @@ module ActiveResource
# Core method for finding resources. Used similarly to Active Record's find method.
#
# 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(:all, :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
# ==== Arguments
# The first argument is considered to be the scope of the query. That is, how many
# resources are returned from the request. It can be one of the following.
#
# +:one+:: Returns a single resource.
# +:first+:: Returns the first resource found.
# +:all+:: Returns every resource that matches the request.
#
# ==== 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)
scope = arguments.slice!(0)
options = arguments.slice!(0) || {}
@ -138,11 +363,38 @@ module ActiveResource
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 = {})
connection.delete(element_path(id, options))
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 = {})
id && !find_single(id, options).nil?
rescue ActiveResource::ResourceNotFound
@ -226,33 +478,79 @@ module ActiveResource
attr_accessor :attributes #: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 = {})
@attributes = {}
@prefix_options = {}
load(attributes)
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?
id.nil?
end
# Get the id of the object.
# Get the +id+ attribute of the resource.
def id
attributes[self.class.primary_key]
end
# Set the id of the object.
# Set the +id+ attribute of the resource.
def id=(id)
attributes[self.class.primary_key] = id
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)
other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id)
end
# Delegates to ==
# Tests for equality (delegates to ==).
def eql?(other)
self == other
end
@ -263,6 +561,22 @@ module ActiveResource
id.hash
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
returning new do |resource|
resource.attributes = @attributes
@ -270,35 +584,137 @@ module ActiveResource
end
end
# Delegates to +create+ if a new object, +update+ if its old. If the response to the save includes a body,
# it will be assumed that this body is XML for the final object as it looked after the save (which would include
# attributes like created_at that wasn't part of the original submit).
# A method to save (+POST+) or update (+PUT+) a resource. It delegates to +create+ if a new object,
# +update+ if it is existing. If the response to the save includes a body, it will be assumed that this body
# 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
new? ? create : update
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
connection.delete(element_path, self.class.headers)
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?
!new? && self.class.exists?(id, :params => prefix_options)
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={})
attributes.to_xml({:root => self.class.element_name}.merge(options))
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
self.load(self.class.find(id, :params => @prefix_options).attributes)
end
# Manually load attributes from a hash. Recursively loads collections of
# resources.
# A method to manually load attributes from a hash. Recursively loads collections of
# 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)
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
@prefix_options, attributes = split_options(attributes)
@ -321,8 +737,9 @@ module ActiveResource
# For checking respond_to? without searching the attributes (which is faster).
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
# person.respond_to?("name?") which will all return true.
# A method to determine if an object responds to a message (e.g., a method call). In Active Resource, a +Person+ object with a
# +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)
method_name = method.to_s
if attributes.nil?

View file

@ -5,7 +5,7 @@ require 'uri'
require 'benchmark'
module ActiveResource
class ConnectionError < StandardError
class ConnectionError < StandardError # :nodoc:
attr_reader :response
def initialize(response, message = nil)
@ -18,20 +18,28 @@ module ActiveResource
end
end
class ClientError < ConnectionError; end # 4xx Client Error
class ResourceNotFound < ClientError; end # 404 Not Found
class ResourceConflict < ClientError; end # 409 Conflict
# 4xx Client Error
class ClientError < ConnectionError; end # :nodoc:
class ServerError < ConnectionError; end # 5xx Server Error
# 404 Not Found
class ResourceNotFound < ClientError; end # :nodoc:
# 409 Conflict
class ResourceConflict < ClientError; end # :nodoc:
# 5xx Server Error
class ServerError < ConnectionError; end # :nodoc:
# 405 Method Not Allowed
class MethodNotAllowed < ClientError
class MethodNotAllowed < ClientError # :nodoc:
def allowed_methods
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
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
attr_reader :site
@ -46,6 +54,8 @@ module ActiveResource
end
end
# The +site+ parameter is required and will set the +site+
# attribute to the URI for the remote resource service.
def initialize(site)
raise ArgumentError, 'Missing site URI' unless site
self.site = site
@ -84,7 +94,6 @@ module ActiveResource
from_xml_data(Hash.from_xml(response.body))
end
private
# Makes request to remote service.
def request(method, path, *arguments)
@ -152,6 +161,5 @@ module ActiveResource
data
end
end
end
end

View file

@ -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 },
# :element => { :promote => :put, :deactivate => :delete }
# :collection => { :active => :get }
# This route set creates routes for the following http requests:
#
# 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
# 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
#
# This module provides the ability for Active Resource to call these
# custom REST methods and get the response back.
# Using this module, Active Resource can use these custom REST methods just like the
# standard methods.
#
# class Person < ActiveResource::Base
# self.site = "http://37s.sunrise.i:3000"
# 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).delete(:deactivate)
# Person.find(1).put(:promote, :position => 'Manager') # PUT /people/1/promote.xml
# 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 CustomMethods
def self.included(within)

View file

@ -14,23 +14,76 @@ module ActiveResource
@base, @errors = base, {}
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)
add(:base, msg)
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)
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
@errors[attribute.to_s] << msg
end
# 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)
!@errors[attribute.to_s].nil?
end
# * Returns nil, if no errors are associated with the specified +attribute+.
# * Returns 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+.
# A method to return the errors associated with +attribute+, 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_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)
errors = @errors[attribute.to_s]
return nil if errors.nil?
@ -39,23 +92,72 @@ module ActiveResource
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
on(:base)
end
# 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
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
# 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".
#
# ==== 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
full_messages.each { |msg| yield msg }
end
# 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
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
# 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
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
end
@ -86,6 +199,7 @@ module ActiveResource
alias_method :count, :size
alias_method :length, :size
# Grabs errors from the XML response.
def from_xml(xml)
clear
humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
@ -102,9 +216,12 @@ module ActiveResource
end
end
# Module to allow validation of ActiveResource objects, which are implemented by overriding +Base#validate+ or its variants.
# Each of these methods can inspect 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). For example:
# Module to allow validation of ActiveResource objects, which creates an Errors instance for every resource.
# Methods are implemented by overriding +Base#validate+ or its variants Each of these methods can inspect
# 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
# self.site = "http://www.localhost.com:3000/"
@ -133,7 +250,6 @@ module ActiveResource
# person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
# person.save # => true (and person is now saved to the remote service)
#
# An Errors object is automatically created for every resource.
module Validations
def self.included(base) # :nodoc:
base.class_eval do
@ -141,6 +257,7 @@ module ActiveResource
end
end
# Validate a resource and save (POST) it to the remote web service.
def save_with_validation
save_without_validation
true
@ -149,6 +266,16 @@ module ActiveResource
false
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?
errors.empty?
end