Upgrade to Rails 2.0.2

Upgraded to Rails 2.0.2, except that we maintain

   vendor/rails/actionpack/lib/action_controller/routing.rb

from Rail 1.2.6 (at least for now), so that Routes don't change. We still
get to enjoy Rails's many new features.

Also fixed a bug in Chunk-handling: disable WikiWord processing in tags (for real this time).
This commit is contained in:
Jacques Distler 2007-12-21 01:48:59 -06:00
parent 0f6889e09f
commit 6873fc8026
1083 changed files with 52810 additions and 41058 deletions

View file

@ -0,0 +1,872 @@
require 'active_resource/connection'
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 type="array"><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 Active Resource calls.
cattr_accessor :logger
class << self
# 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
elsif superclass != Object && superclass.site
superclass.site.dup.freeze
end
end
# 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 = site.nil? ? nil : create_site_uri_from(site)
end
# Sets the format that attributes are sent and received in from a mime type reference. Example:
#
# Person.format = :json
# Person.find(1) # => GET /people/1.json
#
# Person.format = ActiveResource::Formats::XmlFormat
# Person.find(1) # => GET /people/1.xml
#
# Default format is :xml.
def format=(mime_type_reference_or_format)
format = mime_type_reference_or_format.is_a?(Symbol) ?
ActiveResource::Formats[mime_type_reference_or_format] : mime_type_reference_or_format
write_inheritable_attribute("format", format)
connection.format = format
end
# Returns the current format, default is ActiveResource::Formats::XmlFormat
def format # :nodoc:
read_inheritable_attribute("format") || ActiveResource::Formats[:xml]
end
# 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)
if defined?(@connection) || superclass == Object
@connection = Connection.new(site, format) if refresh || @connection.nil?
@connection
else
superclass.connection
end
end
def headers
@headers ||= {}
end
# Do not include any modules in the default element name. This makes it easier to seclude ARes objects
# in a separate namespace without having to set element_name repeatedly.
attr_accessor_with_default(:element_name) { to_s.split("::").last.underscore } #:nodoc:
attr_accessor_with_default(:collection_name) { element_name.pluralize } #:nodoc:
attr_accessor_with_default(:primary_key, 'id') #:nodoc:
# 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] == '/'
# generate the actual method based on the current site path
self.prefix = default
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 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}]}" }
# Redefine the new methods.
code = <<-end_code
def prefix_source() "#{value}" end
def prefix(options={}) "#{prefix_call}" end
end_code
silence_warnings { instance_eval code, __FILE__, __LINE__ }
rescue
logger.error "Couldn't set prefix: #{$!}\n #{code}"
raise
end
alias_method :set_prefix, :prefix= #:nodoc:
alias_method :set_element_name, :element_name= #:nodoc:
alias_method :set_collection_name, :collection_name= #:nodoc:
# 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
#
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}.#{format.extension}#{query_string(query_options)}"
end
# 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
#
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}.#{format.extension}#{query_string(query_options)}"
end
alias_method :set_primary_key, :primary_key= #:nodoc:
# Create a new resource instance and request to the remote service
# 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 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.
#
# ==== 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) || {}
case scope
when :all then find_every(options)
when :first then find_every(options).first
when :one then find_one(options)
else find_single(scope, options)
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
# 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
false
end
private
# Find every resource
def find_every(options)
case from = options[:from]
when Symbol
instantiate_collection(get(from, options[:params]))
when String
path = "#{from}#{query_string(options[:params])}"
instantiate_collection(connection.get(path, headers) || [])
else
prefix_options, query_options = split_options(options[:params])
path = collection_path(prefix_options, query_options)
instantiate_collection( (connection.get(path, headers) || []), prefix_options )
end
end
# Find a single resource from a one-off URL
def find_one(options)
case from = options[:from]
when Symbol
instantiate_record(get(from, options[:params]))
when String
path = "#{from}#{query_string(options[:params])}"
instantiate_record(connection.get(path, headers))
end
end
# Find a single resource from the default URL
def find_single(scope, options)
prefix_options, query_options = split_options(options[:params])
path = element_path(scope, prefix_options, query_options)
instantiate_record(connection.get(path, headers), prefix_options)
end
def instantiate_collection(collection, prefix_options = {})
collection.collect! { |record| instantiate_record(record, prefix_options) }
end
def instantiate_record(record, prefix_options = {})
returning new(record) do |resource|
resource.prefix_options = prefix_options
end
end
# Accepts a URI and creates the site URI from that.
def create_site_uri_from(site)
site.is_a?(URI) ? site.dup : URI.parse(site)
end
# contains a set of the current prefix parameters.
def prefix_parameters
@prefix_parameters ||= prefix_source.scan(/:\w+/).map { |key| key[1..-1].to_sym }.to_set
end
# Builds the query string for the request.
def query_string(options)
"?#{options.to_query}" unless options.nil? || options.empty?
end
# split an option hash into two hashes, one containing the prefix options,
# and the other containing the leftovers.
def split_options(options = {})
prefix_options, query_options = {}, {}
(options || {}).each do |key, value|
next if key.blank?
(prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value
end
[ prefix_options, query_options ]
end
end
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
# 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+ attribute of the resource.
def id
attributes[self.class.primary_key]
end
# Set the +id+ attribute of the resource.
def id=(id)
attributes[self.class.primary_key] = id
end
# Allows ActiveResource objects to be used as parameters in ActionPack URL generation.
def to_param
id && id.to_s
end
# 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
# Tests for equality (delegates to ==).
def eql?(other)
self == other
end
# Delegates to id in order to allow two resources of the same type and id to work with something like:
# [Person.find(1), Person.find(2)] & [Person.find(1), Person.find(4)] # => [Person.find(1)]
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 self.class.new do |resource|
resource.attributes = @attributes
resource.prefix_options = @prefix_options
end
end
# 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
# 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 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
# 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
# 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
# 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)
attributes.each do |key, value|
@attributes[key.to_s] =
case value
when Array
resource = find_or_create_resource_for_collection(key)
value.map { |attrs| resource.new(attrs) }
when Hash
resource = find_or_create_resource_for(key)
resource.new(value)
else
value.dup rescue value
end
end
self
end
# For checking respond_to? without searching the attributes (which is faster).
alias_method :respond_to_without_attributes?, :respond_to?
# 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?
return super
elsif attributes.has_key?(method_name)
return true
elsif ['?','='].include?(method_name.last) && attributes.has_key?(method_name.first(-1))
return true
end
# super must be called at the end of the method, because the inherited respond_to?
# would return true for generated readers, even if the attribute wasn't present
super
end
protected
def connection(refresh = false)
self.class.connection(refresh)
end
# Update the resource on the remote service.
def update
returning connection.put(element_path(prefix_options), to_xml, self.class.headers) do |response|
load_attributes_from_response(response)
end
end
# Create (i.e., save to the remote service) the new resource.
def create
returning connection.post(collection_path, to_xml, self.class.headers) do |response|
self.id = id_from_response(response)
load_attributes_from_response(response)
end
end
def load_attributes_from_response(response)
if response['Content-Length'] != "0" && response.body.strip.size > 0
load(self.class.format.decode(response.body))
end
end
# Takes a response from a typical create post and pulls the ID out
def id_from_response(response)
response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1]
end
def element_path(options = nil)
self.class.element_path(id, options || prefix_options)
end
def collection_path(options = nil)
self.class.collection_path(options || prefix_options)
end
private
# Tries to find a resource for a given collection name; if it fails, then the resource is created
def find_or_create_resource_for_collection(name)
find_or_create_resource_for(name.to_s.singularize)
end
# Tries to find a resource for a given name; if it fails, then the resource is created
def find_or_create_resource_for(name)
resource_name = name.to_s.camelize
# FIXME: Make it generic enough to support any depth of module nesting
if (ancestors = self.class.name.split("::")).size > 1
begin
ancestors.first.constantize.const_get(resource_name)
rescue NameError
self.class.const_get(resource_name)
end
else
self.class.const_get(resource_name)
end
rescue NameError
resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
resource.prefix = self.class.prefix
resource.site = self.class.site
resource
end
def split_options(options = {})
self.class.send!(:split_options, options)
end
def method_missing(method_symbol, *arguments) #:nodoc:
method_name = method_symbol.to_s
case method_name.last
when "="
attributes[method_name.first(-1)] = arguments.first
when "?"
attributes[method_name.first(-1)]
else
attributes.has_key?(method_name) ? attributes[method_name] : super
end
end
end
end

View file

@ -0,0 +1,172 @@
require 'net/https'
require 'date'
require 'time'
require 'uri'
require 'benchmark'
module ActiveResource
class ConnectionError < StandardError # :nodoc:
attr_reader :response
def initialize(response, message = nil)
@response = response
@message = message
end
def to_s
"Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
end
end
# 3xx Redirection
class Redirection < ConnectionError # :nodoc:
def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
end
# 4xx Client Error
class ClientError < ConnectionError; end # :nodoc:
# 400 Bad Request
class BadRequest < ClientError; end # :nodoc
# 401 Unauthorized
class UnauthorizedAccess < ClientError; end # :nodoc
# 403 Forbidden
class ForbiddenAccess < ClientError; end # :nodoc
# 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 # :nodoc:
def allowed_methods
@response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
end
end
# 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
attr_accessor :format
class << self
def requests
@@requests ||= []
end
end
# The +site+ parameter is required and will set the +site+
# attribute to the URI for the remote resource service.
def initialize(site, format = ActiveResource::Formats[:xml])
raise ArgumentError, 'Missing site URI' unless site
self.site = site
self.format = format
end
# Set URI for remote service.
def site=(site)
@site = site.is_a?(URI) ? site : URI.parse(site)
end
# Execute a GET request.
# Used to get (find) resources.
def get(path, headers = {})
format.decode(request(:get, path, build_request_headers(headers)).body)
end
# Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
# Used to delete resources.
def delete(path, headers = {})
request(:delete, path, build_request_headers(headers))
end
# Execute a PUT request (see HTTP protocol documentation if unfamiliar).
# Used to update resources.
def put(path, body = '', headers = {})
request(:put, path, body.to_s, build_request_headers(headers))
end
# Execute a POST request.
# Used to create new resources.
def post(path, body = '', headers = {})
request(:post, path, body.to_s, build_request_headers(headers))
end
private
# Makes request to remote service.
def request(method, path, *arguments)
logger.info "#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}" if logger
result = nil
time = Benchmark.realtime { result = http.send(method, path, *arguments) }
logger.info "--> #{result.code} #{result.message} (#{result.body ? result.body : 0}b %.2fs)" % time if logger
handle_response(result)
end
# Handles response and error codes from remote service.
def handle_response(response)
case response.code.to_i
when 301,302
raise(Redirection.new(response))
when 200...400
response
when 400
raise(BadRequest.new(response))
when 401
raise(UnauthorizedAccess.new(response))
when 403
raise(ForbiddenAccess.new(response))
when 404
raise(ResourceNotFound.new(response))
when 405
raise(MethodNotAllowed.new(response))
when 409
raise(ResourceConflict.new(response))
when 422
raise(ResourceInvalid.new(response))
when 401...500
raise(ClientError.new(response))
when 500...600
raise(ServerError.new(response))
else
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
end
end
# Creates new Net::HTTP instance for communication with
# remote service and resources.
def http
http = Net::HTTP.new(@site.host, @site.port)
http.use_ssl = @site.is_a?(URI::HTTPS)
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
http
end
def default_header
@default_header ||= { 'Content-Type' => format.mime_type }
end
# Builds headers for request to remote service.
def build_request_headers(headers)
authorization_header.update(default_header).update(headers)
end
# Sets authorization header; authentication information is pulled from credentials provided with site URI.
def authorization_header
(@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
end
def logger #:nodoc:
ActiveResource::Base.logger
end
end
end

View file

@ -0,0 +1,105 @@
# 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:
#
# map.resources :people, :new => { :register => :post },
# :element => { :promote => :put, :deactivate => :delete }
# :collection => { :active => :get }
#
# This route set 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
#
# 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) # 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.get(:active) # GET /people/active.xml
# # => [{:id => 1, :name => 'Ryan'}, {:id => 2, :name => 'Joe'}]
#
module ActiveResource
module CustomMethods
def self.included(within)
within.class_eval do
extend ActiveResource::CustomMethods::ClassMethods
include ActiveResource::CustomMethods::InstanceMethods
class << self
alias :orig_delete :delete
def get(method_name, options = {})
connection.get(custom_method_collection_url(method_name, options), headers)
end
def post(method_name, options = {}, body = '')
connection.post(custom_method_collection_url(method_name, options), body, headers)
end
def put(method_name, options = {}, body = '')
connection.put(custom_method_collection_url(method_name, options), body, headers)
end
# Need to jump through some hoops to retain the original class 'delete' method
def delete(custom_method_name, options = {})
if (custom_method_name.is_a?(Symbol))
connection.delete(custom_method_collection_url(custom_method_name, options), headers)
else
orig_delete(custom_method_name, options)
end
end
end
end
end
module ClassMethods
def custom_method_collection_url(method_name, options = {})
prefix_options, query_options = split_options(options)
"#{prefix(prefix_options)}#{collection_name}/#{method_name}.xml#{query_string(query_options)}"
end
end
module InstanceMethods
def get(method_name, options = {})
connection.get(custom_method_element_url(method_name, options), self.class.headers)
end
def post(method_name, options = {}, body = '')
if new?
connection.post(custom_method_new_element_url(method_name, options), (body.nil? ? to_xml : body), self.class.headers)
else
connection.post(custom_method_element_url(method_name, options), body, self.class.headers)
end
end
def put(method_name, options = {}, body = '')
connection.put(custom_method_element_url(method_name, options), body, self.class.headers)
end
def delete(method_name, options = {})
connection.delete(custom_method_element_url(method_name, options), self.class.headers)
end
private
def custom_method_element_url(method_name, options = {})
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/#{id}/#{method_name}.xml#{self.class.send!(:query_string, options)}"
end
def custom_method_new_element_url(method_name, options = {})
"#{self.class.prefix(prefix_options)}#{self.class.collection_name}/new/#{method_name}.xml#{self.class.send!(:query_string, options)}"
end
end
end
end

View file

@ -0,0 +1,14 @@
module ActiveResource
module Formats
# Lookup the format class from a mime type reference symbol. Example:
#
# ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
# ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
def self.[](mime_type_reference)
ActiveResource::Formats.const_get(mime_type_reference.to_s.camelize + "Format")
end
end
end
require 'active_resource/formats/xml_format'
require 'active_resource/formats/json_format'

View file

@ -0,0 +1,23 @@
module ActiveResource
module Formats
module JsonFormat
extend self
def extension
"json"
end
def mime_type
"application/json"
end
def encode(hash)
hash.to_json
end
def decode(json)
ActiveSupport::JSON.decode(json)
end
end
end
end

View file

@ -0,0 +1,34 @@
module ActiveResource
module Formats
module XmlFormat
extend self
def extension
"xml"
end
def mime_type
"application/xml"
end
def encode(hash)
hash.to_xml
end
def decode(xml)
from_xml_data(Hash.from_xml(xml))
end
private
# Manipulate from_xml Hash, because xml_simple is not exactly what we
# want for ActiveResource.
def from_xml_data(data)
if data.is_a?(Hash) && data.keys.size == 1
data.values.first
else
data
end
end
end
end
end

View file

@ -0,0 +1,147 @@
require 'active_resource/connection'
module ActiveResource
class InvalidRequestError < StandardError; end #:nodoc:
class HttpMock
class Responder
def initialize(responses)
@responses = responses
end
for method in [ :post, :put, :get, :delete ]
module_eval <<-EOE
def #{method}(path, request_headers = {}, body = nil, status = 200, response_headers = {})
@responses[Request.new(:#{method}, path, nil, request_headers)] = Response.new(body || "", status, response_headers)
end
EOE
end
end
class << self
def requests
@@requests ||= []
end
def responses
@@responses ||= {}
end
def respond_to(pairs = {})
reset!
pairs.each do |(path, response)|
responses[path] = response
end
if block_given?
yield Responder.new(responses)
else
Responder.new(responses)
end
end
def reset!
requests.clear
responses.clear
end
end
for method in [ :post, :put ]
module_eval <<-EOE
def #{method}(path, body, headers)
request = ActiveResource::Request.new(:#{method}, path, body, headers)
self.class.requests << request
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request.inspect}"))
end
EOE
end
for method in [ :get, :delete ]
module_eval <<-EOE
def #{method}(path, headers)
request = ActiveResource::Request.new(:#{method}, path, nil, headers)
self.class.requests << request
self.class.responses[request] || raise(InvalidRequestError.new("No response recorded for: \#{request.inspect}"))
end
EOE
end
def initialize(site)
@site = site
end
end
class Request
attr_accessor :path, :method, :body, :headers
def initialize(method, path, body = nil, headers = {})
@method, @path, @body, @headers = method, path, body, headers.dup
@headers.update('Content-Type' => 'application/xml')
end
def ==(other_request)
other_request.hash == hash
end
def eql?(other_request)
self == other_request
end
def to_s
"<#{method.to_s.upcase}: #{path} [#{headers}] (#{body})>"
end
def hash
"#{path}#{method}#{headers}".hash
end
end
class Response
attr_accessor :body, :message, :code, :headers
def initialize(body, message = 200, headers = {})
@body, @message, @headers = body, message.to_s, headers
@code = @message[0,3].to_i
resp_cls = Net::HTTPResponse::CODE_TO_OBJ[@code.to_s]
if resp_cls && !resp_cls.body_permitted?
@body = nil
end
if @body.nil?
self['Content-Length'] = "0"
else
self['Content-Length'] = body.size.to_s
end
end
def success?
(200..299).include?(code)
end
def [](key)
headers[key]
end
def []=(key, value)
headers[key] = value
end
def ==(other)
if (other.is_a?(Response))
other.body == body && other.message == message && other.headers == headers
else
false
end
end
end
class Connection
private
silence_warnings do
def http
@http ||= HttpMock.new(@site)
end
end
end
end

View file

@ -0,0 +1,288 @@
module ActiveResource
class ResourceInvalid < ClientError #:nodoc:
end
# Active Resource validation is reported to and from this object, which is used by Base#save
# to determine whether the object in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
attr_reader :errors
delegate :empty?, :to => :errors
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
# 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?
errors.size == 1 ? errors.first : errors
end
alias :[] :on
# 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 = []
@errors.each_key do |attr|
@errors[attr].each do |msg|
next if msg.nil?
if attr == "base"
full_messages << msg
else
full_messages << [attr.humanize, msg].join(' ')
end
end
end
full_messages
end
def clear
@errors = {}
end
# 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
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) }
messages = Hash.from_xml(xml)['errors']['error'] rescue []
messages.each do |message|
attr_message = humanized_attributes.keys.detect do |attr_name|
if message[0, attr_name.size + 1] == "#{attr_name} "
add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
end
end
add_to_base message if attr_message.nil?
end
end
end
# 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/"
# protected
# def validate
# errors.add_on_empty %w( first_name last_name )
# errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
# end
#
# def validate_on_create # is only run the first time a new object is saved
# unless valid_member?(self)
# errors.add("membership_discount", "has expired")
# end
# end
#
# def validate_on_update
# errors.add_to_base("No changes have occurred") if unchanged_attributes?
# end
# end
#
# person = Person.new("first_name" => "Jim", "phone_number" => "I will not tell you.")
# person.save # => false (and doesn't do the save)
# person.errors.empty? # => false
# person.errors.count # => 2
# person.errors.on "last_name" # => "can't be empty"
# person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
# person.save # => true (and person is now saved to the remote service)
#
module Validations
def self.included(base) # :nodoc:
base.class_eval do
alias_method_chain :save, :validation
end
end
# Validate a resource and save (POST) it to the remote web service.
def save_with_validation
save_without_validation
true
rescue ResourceInvalid => error
errors.from_xml(error.response.body)
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
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)
end
end
end

View file

@ -0,0 +1,9 @@
module ActiveResource
module VERSION #:nodoc:
MAJOR = 2
MINOR = 0
TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
end