require 'rubygems'
require 'hpricot'
require 'date'
module Relax
# Response is intended to be a parent class for responses passed to
# Service#call.
#
# A response is in essence an object used to facilitate XML parsing. It
# stores an XML document, and provides access to it through methods like
# #element and #attribute.
class Response
attr_accessor :raw
attr_accessor :xml
# New takes in the XML from the response. For the initial response, this
# will be the root element, but child elements may also be passed into
# Response objects.
#
# This will raise a MissingParameter error if a parameterd marked as
# required is not present in the XML response.
def initialize(xml)
@raw = xml
@xml = Hpricot.XML(xml.to_s)
if parameters = self.class.instance_variable_get('@parameters')
parameters.each do |parameter, options|
begin
element = options[:element] || parameter
if attribute = options[:attribute] and attribute == true
node = attribute(root, element)
elsif attribute
node = attribute(element(element), attribute)
elsif options[:collection]
node = elements(element)
else
node = element(element)
end
if options[:collection]
value = node.collect do |element|
options[:collection].new(element)
end
else
case type = options[:type]
when Response
value = type.new(node)
when :date
value = date_value(node)
when :time
value = time_value(node)
when :float
value = float_value(node)
when :integer
value = integer_value(node)
when :text
else
value = text_value(node)
end
end
instance_variable_set("@#{parameter}", value)
rescue Hpricot::Error
raise MissingParameter if options[:required]
end
end
end
end
# Returns the root of the XML document.
def root
@xml.root
end
# Checks the name of the root node.
def is?(name)
root.name.gsub(/.*:(.*)/, '\1') == node_name(name)
end
# Returns an element of the specified name.
def element(name)
root.at(root_path(name))
end
alias :has? :element
# Returns an attribute on an element.
def attribute(element, name)
element[name]
end
# Returns a set of elements matching name.
def elements(name)
root.search(root_path(name))
end
# Gets the value of an element or attribute.
def value(value)
value.is_a?(Hpricot::Elem) ? value.inner_text : value.to_s
end
# Gets a text value.
def text_value(value)
value(value)
end
# Gets an integer value.
def integer_value(value)
value(value).to_i
end
# Gets a float value.
def float_value(value)
value(value).to_f
end
# Gets a date value.
def date_value(value)
Date.parse(value(value))
end
# Gets a time value.
def time_value(value)
Time.parse(value(value))
end
class << self
# When a Response is extended, the superclasses parameters are copied
# into the new class. This behavior has the following side-effect: if
# parameters are added to the superclass after it has been extended,
# those new paramters won't be passed on to its children. This shouldn't
# be a problem in most cases.
def inherited(subclass)
@parameters.each do |name, options|
subclass.parameter(name, options)
end if @parameters
end
# Specifes a parameter that will be automatically parsed when the
# Response is instantiated.
#
# Options:
# - :attribute: An attribute name to use, or true to
# use the :element value as the attribute name on the root.
# - :collection: A class used to instantiate each item when
# selecting a collection of elements.
# - :element: The XML element name.
# - :object: A class used to instantiate an element.
# - :type: The type of the parameter. Should be one of
# :text, :integer, :float, or :date.
def parameter(name, options = {})
attr_accessor name
@parameters ||= {}
@parameters[name] = options
end
def ===(response)
response.is_a?(Class) ? response.ancestors.include?(self) : super
end
end
private
# Converts a name to a node name.
def node_name(name)
name.to_s
end
# Gets the XPath expression representing the root node.
def root_path(name)
"/#{node_name(name)}"
end
end
end