Skip to content
Browse files

Big documentation upgrade for ARes (closes #8694) [jeremymcanally]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7098 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 753cbf1 commit ae4838fff20857b11b1092d82b34ef7d32edfcab @dhh dhh committed
View
144 activeresource/README
@@ -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
-
- # 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
-
- # 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
+As you can see, the methods are quite similar to Active Record's methods for dealing with database
+records. But rather than dealing with
- # And destruction
- # DELETE http://api.people.com:3000/people/1.xml
- #
- ryan = Person.find(1)
- ryan.destroy #=> true # Or Person.delete(ryan.id)
+==== Protocol
+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:
-=== Protocol
+* 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.
-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
-
-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
+You can find more usage information in the ActiveResource::Base documentation.
-For obvious reasons it is best if such services are available over https.
View
511 activeresource/lib/active_resource/base.rb
@@ -3,12 +3,155 @@
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 @@ def site
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 @@ def headers
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 @@ def prefix(options={})
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 @@ def prefix(options={}) "#{prefix_call}" end
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,30 +280,77 @@ def collection_path(prefix_options = {}, query_options = nil)
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 }
end
# 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 @@ def find(*arguments)
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 @@ def split_options(options = {})
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 @@ def hash
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 @@ def dup
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 @@ def load(attributes)
# 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
28 activeresource/lib/active_resource/connection.rb
@@ -5,7 +5,7 @@
require 'benchmark'
module ActiveResource
- class ConnectionError < StandardError
+ class ConnectionError < StandardError # :nodoc:
attr_reader :response
def initialize(response, message = nil)
@@ -18,20 +18,28 @@ def to_s
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:
+
+ # 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
- 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 @@ class << self ; attr_reader :default_header 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)
raise ArgumentError, 'Missing site URI' unless site
self.site = site
@@ -84,7 +94,6 @@ def xml_from_response(response)
from_xml_data(Hash.from_xml(response.body))
end
-
private
# Makes request to remote service.
def request(method, path, *arguments)
@@ -152,6 +161,5 @@ def from_xml_data(data)
data
end
end
-
end
-end
+end
View
37 activeresource/lib/active_resource/custom_methods.rb
@@ -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') # PUT /people/1/promote.xml
+# Person.find(1).delete(:deactivate) # DELETE /people/1/deactivate.xml
#
-# Person.find(1).put(:promote, :position => 'Manager')
-# Person.find(1).delete(:deactivate)
+# 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
143 activeresource/lib/active_resource/validations.rb
@@ -14,23 +14,76 @@ def initialize(base) # :nodoc:
@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 @@ def on(attribute)
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 @@ def clear
# 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 @@ def size
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 @@ def from_xml(xml)
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 @@ def from_xml(xml)
# 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 @@ def self.included(base) # :nodoc:
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 @@ def save_with_validation
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

0 comments on commit ae4838f

Please sign in to comment.
Something went wrong with that request. Please try again.