# wadl.rb
# http://www.crummy.com/software/wadl.rb/
# Super cheap Ruby WADL client
# by Leonard Richardson leonardr@segfault.org
# v20070217
# For more on WADL, see http://wadl.dev.java.net/
require 'rubygems'
require 'rest-open-uri'
require 'delegate'
require 'rexml/document'
require 'set'
require 'cgi'
begin
require 'rubygems'
require 'mime/types'
MIME_TYPES_SUPPORTED = true
rescue LoadError
MIME_TYPES_SUPPORTED = false
end
module WADL
# A container for application-specific faults
module Faults
end
#########################################################################
#
# A cheap way of defining an XML schema as Ruby classes and then parsing
# documents into instances of those classes.
class CheapSchema
attr_accessor :index_key, :href
@may_be_reference = false
@contents_are_mixed_data = false
def initialize
@attributes = {}
@contents = nil
end
def self.init
@names = {}
@members = {}
@collections = {}
@required_attributes = []
@attributes = []
end
def self.inherit(from)
init
@names = from.names.dup if from.names
@members = from.members.dup if from.members
@collections = from.collections.dup if from.collections
@required_attributes = from.required_attributes.dup if from.required_attributes
@attributes = from.attributes.dup if from.attributes
end
def self.inherited(klass)
klass.inherit(self)
end
def self.names
@names
end
def self.members
@members
end
def self.collections
@collections
end
def self.required_attributes
@required_attributes
end
def self.attributes
@attributes
end
def attributes
@attributes
end
def self.may_be_reference?
@may_be_reference
end
def self.in_document(element_name)
@names[:element] = element_name
@names[:member] = element_name
@names[:collection] = element_name + 's'
end
def self.as_collection(collection_name)
@names[:collection] = collection_name
end
def self.as_member(member_name)
@names[:member] = member_name
end
def self.contents_are_mixed_data
@contents_are_mixed_data = true
end
def self.has_one(*classes)
classes.each do |c|
@members[c.names[:element]] = c
member_name = c.names[:member]
dereferencing_instance_accessor member_name
end
end
def self.has_many(*classes)
classes.each do |c|
@collections[c.names[:element]] = c
collection_name = c.names[:collection]
dereferencing_instance_accessor collection_name
find_method_name = "find_#{c.names[:element]}"
# Define a method for finding a specific element of this
# collection.
# TODO: In Ruby 1.9, make match_block a block argument.
define_method(find_method_name) do |name, *args|
name = name.to_s
if args[0].respond_to? :call
match_block = args[0]
else
match_block = Proc.new { |m| m.name_matches(name) }
end
unless args[1].nil?
auto_dereference = args[1]
else
auto_dereference = true
end
match = self.send(collection_name).detect do |m|
match_block.call(m) || \
(c.may_be_reference? && auto_dereference &&
match_block.call(m.dereference))
end
match = match.dereference if match && auto_dereference
return match
end
end
end
def self.dereferencing_instance_accessor(*symbols)
symbols.each do |name|
define_method(name) do
dereference.instance_variable_get("@#{name}")
end
define_method(name.to_s+'=') do |value|
dereference.instance_variable_set("@#{name}", value)
end
end
end
def self.dereferencing_attr_accessor(*symbols)
symbols.each do |name|
m = instance_methods
define_method(name) do
dereference.attributes[name.to_s]
end
define_method(name.to_s+'=') do |value|
dereference.attributes[name.to_s] = value
end
end
end
def self.has_attributes(*names)
names.each do |name|
@attributes << name
@index_attribute ||= name.to_s
if name == :href
attr_accessor name
else
dereferencing_attr_accessor name
end
end
end
def self.has_required(*names)
names.each do |name|
@required_attributes << name
@index_attribute ||= name.to_s
if name == :href
attr_accessor name
else
dereferencing_attr_accessor name
end
end
end
def self.may_be_reference
@may_be_reference = true
define_method("dereference") do
return self if not self.attributes['href']
unless @referenced
if self.attributes['href']
find_method_name = "find_#{self.class.names[:element]}"
p = self
until @referenced or !p do
begin
p = p.parent
end until !p or p.respond_to? find_method_name
if p
@referenced = p.send(find_method_name, self.attributes['href'], nil, false) if p
else
@referenced = nil
end
end
end
end
@referenced ? dereference_with_context(@referenced) : nil
end
end
# This object is a reference to another object. This method returns
# an object that acts like the other object, but also contains any
# neccessary context about this object. See the ResourceAndAddress
# implementation, in which a dereferenced resource contains
# information about the parent of the resource that referenced it
# (otherwise, there's no way to build the URI).
def dereference_with_context(referent)
referent
end
# Turn an XML element into an instance of this class.
def self.from_element(parent, e, need_finalization)
attributes = e.attributes
me = self.new
me.parent = parent
@collections.each do |name, clazz|
collection_name = "@" + clazz.names[:collection].to_s
me.instance_variable_set(collection_name, [])
end
if @may_be_reference and attributes['href']
# Handle objects that are just references to other objects
# somewhere above this one in the hierarchy
href = attributes['href']
if href[0] == ?#
href = href[1..href.size]
else
puts "Warning: HREF #{href} should be ##{href}"
end
me.attributes['href'] = href
else
# Handle this element's attributes
@required_attributes.each do |name|
name = name.to_s
unless attributes[name]
raise ArgumentError, %{Missing required attribute "#{name}" in element: #{e}}
end
#puts " #{name}=#{attributes[name]}"
me.attributes[name.to_s] = attributes[name]
me.index_key = attributes[name] if name == @index_attribute
end
@attributes.each do |name|
name = name.to_s
#puts " #{name}=#{attributes[name]}"
me.attributes[name.to_s] = attributes[name]
me.index_key = attributes[name] if name == @index_attribute
end
end
# Handle this element's children.
if @contents_are_mixed_data
me.instance_variable_set('@contents', e.children)
else
e.each_element do |child|
clazz = @members[child.name] || @collections[child.name]
if clazz
object = clazz.from_element(me, child, need_finalization)
if clazz == @members[child.name]
#puts "#{self.name} can have one #{clazz.name}"
instance_variable_name = "@" + clazz.names[:member].to_s
if me.instance_variable_get(instance_variable_name)
raise "#{self.name} can only have one #{clazz.name}, but several were specified in element: #{e}"
end
#puts "Setting its #{instance_variable_name} to a #{object.class.name}"
me.instance_variable_set(instance_variable_name, object)
else
#puts "#{self.name} can have many #{clazz.name}"
collection_name = "@" + clazz.names[:collection].to_s
collection = me.instance_variable_get(collection_name)
#puts "Adding a #{object.class.name} to #{collection_name} collection"
collection << object
end
end
end
end
need_finalization << me if me.respond_to? :finalize_creation
return me
end
# Common instance methods
attr_accessor :parent
# A null implementation so that foo.dereference will always return the
# "real" object.
def dereference
self
end
# Returns whether or not the given name matches this object.
# By default, checks the index key for this class.
def name_matches(name)
index_key == name
end
def to_s(indent=0)
s = ""
i = " " * indent
s << "#{i}#{self.class.name}\n"
if self.class.may_be_reference? and self.attributes['href']
s << "#{i} href=#{self.attributes['href']}\n"
else
[self.class.required_attributes, self.class.attributes].each do |list|
list.each do |attr|
attr = attr.to_s
s << "#{i} #{attr}=#{self.attributes[attr]}\n" if self.attributes[attr]
end
end
self.class.members.each_value do |member_class|
o = self.send(member_class.names[:member])
s << o.to_s(indent+1) if o
end
self.class.collections.each_value do |collection_class|
c = self.send(collection_class.names[:collection])
if c and not c.empty?
s << "#{i} Collection of #{c.size} #{collection_class.name}(s)\n"
c.each do |o|
s << o.to_s(indent+2)
end
end
end
if @contents && !@contents.empty?
s << '-' * 80 << "\n" << @contents.join(' ') << "\n" << '-' * 80 << "\n"
end
end
return s
end
end
#########################################################################
# Classes to keep track of the logical structure of a URI.
URIParts = Struct.new(:uri, :query, :headers)
class URIParts
def to_s
u = uri.dup
unless query.empty?
u << (uri.index('?') ? '&' : '?')
u << query_string
end
u
end
def inspect
s = to_s
s << " Plus headers: #{headers.inspect}" if headers
end
def query_string
query.join('&')
end
def hash(x)
to_str.hash
end
def ==(x)
return to_str == x if x.respond_to? :to_str
return super
end
alias :to_str :to_s
end
# The Address class keeps track of the user's path through a resource
# graph. Values for WADL parameters may be specified at any time using
# the bind method. An Address cannot be turned into a URI and header
# set until all required parameters have been bound to values.
#
# An Address object is built up through calls to Resource#address
class Address
attr_reader :path_fragments, :query_vars, :headers, \
:path_params, :query_params, :header_params
def initialize(path_fragments=[], query_vars=[], headers={},
path_params={}, query_params={}, header_params={})
@path_fragments = path_fragments
@query_vars, @headers = query_vars, headers
@path_params, @query_params, @header_params = path_params, query_params, header_params
end
def _deep_copy_hash(h)
a = h.inject({}) { |h,kv| h[kv[0]] = (kv[1] ? kv[1].dup : kv[1]); h }
end
def _deep_copy_array(a)
a.inject([]) { |a,e| a << (e ? e.dup : e) }
end
# Perform a deep copy.
def deep_copy
Address.new(_deep_copy_array(@path_fragments),
_deep_copy_array(@query_vars), _deep_copy_hash(@headers),
@path_params.dup, @query_params.dup,
@header_params.dup)
end
def to_s
s = "Address:\n"
s << " Path fragments: #{@path_fragments.inspect}\n"
s << " Query variables: #{@query_vars.inspect}\n"
s << " Header variables: #{@headers.inspect}\n"
s << " Unbound path parameters: #{@path_params.inspect}\n"
s << " Unbound query parameters: #{@query_params.inspect}\n"
s << " Unbound header parameters: #{@header_params.inspect}\n"
end
alias :inspect :to_s
def self.embedded_param_names(fragment)
fragment.scan(/\{([^}]+)\}/).flatten
end
# Binds some or all of the unbound variables in this address to values.
def bind!(args={})
path_var_values = args[:path] || {}
query_var_values = args[:query] || {}
header_var_values = args[:headers] || {}
# Bind variables found in the path fragments.
if path_var_values
path_params_to_delete = []
path_fragments.each do |fragment|
if fragment.respond_to? :to_str
# This fragment is a string which might contain {} substitutions.
# Make any substitutions available to the provided path variables.
embedded_param_names = self.class.embedded_param_names(fragment)
embedded_param_names.each do |param_name|
value = path_var_values[param_name] || path_var_values[param_name.to_sym]
param = path_params[param_name]
if param
value = param % value
path_params_to_delete << param
else
value = Param.default.%(value, param_name)
end
fragment.gsub!('{' + param_name + '}', value)
end
else
# This fragment is an array of Param objects (style 'matrix'
# or 'plain') which may be bound to strings. As substitutions
# happen, the array will become a mixed array of Param objects
# and strings.
fragment.each_with_index do |param, i|
if param.respond_to? :name
value = path_var_values[param.name] || path_var_values[param.name.to_sym]
new_value = param % value
fragment[i] = new_value if new_value
path_params_to_delete << param
end
end
end
end
# Delete any embedded path parameters that are now bound from
# our list of unbound parameters.
path_params_to_delete.each { |p| path_params.delete(p.name) }
end
# Bind query variable values to query parameters
query_var_values.each do |name, value|
param = query_params[name.to_s]
if param
query_vars << param % value
query_params.delete(name.to_s)
end
end
# Bind header variables to header parameters
header_var_values.each do |name, value|
param = header_params[name.to_s]
if param
headers[name] = param % value
header_params.delete(name.to_s)
end
end
return self
end
def uri(args={})
obj = deep_copy
obj.bind!(args)
# Build the path
uri = ''
obj.path_fragments.flatten.each do |fragment|
if fragment.respond_to? :to_str
embedded_param_names = self.class.embedded_param_names(fragment)
unless embedded_param_names.empty?
raise ArgumentError, %{Missing a value for required path parameter "#{embedded_param_names[0]}"!}
end
unless fragment.empty?
uri << '/' if !uri.empty? && uri[-1] != ?/
uri << fragment
end
elsif fragment.required
# This is a required Param that was never bound to a value.
raise ArgumentError, %{Missing a value for required path parameter "#{fragment.name}"!}
end
end
# Hunt for required unbound query parameters.
obj.query_params.each do |name, value|
if value.required
raise ArgumentError, %{Missing a value for required query parameter "#{value.name}"!}
end
end
# Hunt for required unbound header parameters.
obj.header_params.each do |name, value|
if value.required
raise ArgumentError, %{Missing a value for required header parameter "#{value.name}"!}
end
end
return URIParts.new(uri, obj.query_vars, obj.headers)
end
end
#########################################################################
#
# Now we use Ruby classes to define the structure of a WADL document
class Documentation < CheapSchema
in_document 'doc'
as_member 'doc'
as_collection 'docs'
has_attributes "xml:lang", :title
contents_are_mixed_data
end
class HasDocs < CheapSchema
has_many Documentation
# Convenience method to define a no-argument singleton method on
# this object.
def define_singleton(name, contents)
return if name =~ /[^A-Za-z0-9_]/
instance_eval(%{def #{name}
#{contents}
end})
end
end
class Option < HasDocs
in_document 'option'
as_member 'option'
as_collection 'options'
has_required :value
end
class Link < HasDocs
in_document 'link'
as_member 'link'
as_collection 'links'
has_attributes :href, :rel, :rev
end
class Param < HasDocs
in_document 'param'
as_member 'param'
as_collection 'params'
has_required :name
has_attributes :type, :default, :style, :path, :required, :repeating, :fixed
has_many Option
has_many Link
def inspect
%{Param "#{name}"}
end
# Validates and formats a proposed value for this parameter. Returns
# the formatted value. Raises an ArgumentError if the value
# is invalid.
#
# The 'name' and 'style' arguments are used in conjunction with the
# default Param object.
def %(value, name=nil, style=nil)
name ||= self.name
style ||= self.style
value = fixed if fixed
unless value
if default
value = default
elsif required
raise ArgumentError, "No value provided for required param \"#{name}\"!"
else
return '' # No value provided and none required.
end
end
if value.respond_to?(:each) && !value.respond_to?(:to_str)
if repeating
values = value
else
raise ArgumentError, "Multiple values provided for single-value param \"#{name}\""
end
else
values = [value]
end
# If the param lists acceptable values in option tags, make sure that
# all values are found in those tags.
if options && !options.empty?
values.each do |value|
unless find_option(value)
acceptable = options.collect { |o| o.value }.join('", "')
raise ArgumentError, %{"#{value}" is not among the acceptable parameter values ("#{acceptable}")}
end
end
end
if style == 'query' || parent.is_a?(RequestFormat) ||
(parent.respond_to?('is_form_representation?') \
&& parent.is_form_representation?)
value = values.collect do |v|
URI.escape(name) + '=' + URI.escape(v.to_s)
end.join('&')
elsif self.style == 'matrix'
if type == 'xsd:boolean'
value = values.collect { |v| (v == 'true' || v == true) ? ';' + name : '' }.join('')
else
value = values.collect do |v|
v ? ';' + URI.escape(name) + '=' + URI.escape(v.to_s) : ''
end.join('')
end
elsif self.style == 'header'
value = values.join(',')
else
# All other cases: plain text representation.
value = values.collect { |v| URI.escape(v.to_s) }.join(',')
end
return value
end
# A default Param object to use for a path parameter that is
# only specified as a name in the path of a resource.
@@default = Param.new
@@default.required = true
@@default.style = 'plain'
@@default.type = 'xsd:string'
def self.default
@@default
end
end
# A mixin for objects that contain representations
module RepresentationContainer
def find_representation_by_media_type(type)
representations.detect { |r| r.mediaType == type }
end
def find_form
representations.detect { |r| r.is_form_representation? }
end
end
class RepresentationFormat < HasDocs
in_document 'representation'
as_collection 'representations'
may_be_reference
has_attributes :id, :mediaType, :element
has_many Param
def is_form_representation?
return mediaType == 'application/x-www-form-encoded' ||
mediaType == 'multipart/form-data'
end
# Creates a representation by plugging a set of parameters
# into a representation format.
def %(values)
if mediaType == 'application/x-www-form-encoded'
representation = []
params.each do |param|
if param.fixed
p_values = [param.fixed]
elsif values[param.name] || values[param.name.to_sym]
p_values = values[param.name] || values[param.name.to_sym]
if !param.repeating || !(p_values.respond_to?(:each) && !p_values.respond_to?(:to_str))
p_values = [p_values]
end
else
if param.required
raise ArgumentError, "Your proposed representation is missing a value for #{param.name}"
end
end
if p_values
p_values.each do |value|
representation << CGI::escape(param.name) + '=' + CGI::escape(value.to_s)
end
end
end
representation = representation.join('&')
else
raise Exception,
"wadl.rb can't instantiate a representation of type #{mediaType}"
end
return representation
end
end
class FaultFormat < RepresentationFormat
in_document 'fault'
as_collection 'faults'
may_be_reference
has_attributes :id, :mediaType, :element, :status
has_many Param
attr_writer :subclass
def subclass
if attributes['href']
dereference.subclass
else
@subclass
end
end
# Define a custom subclass for this fault, so that the programmer
# can rescue this particular fault.
def self.from_element(*args)
me = super
return me if me.attributes['href']
name = me.attributes['id']
if name
begin
c = Class.new(Fault)
WADL::Faults.const_set(name, c) unless WADL::Faults.const_defined? name
me.subclass = c
rescue NameError => e
# This fault format's ID can't be a class name. Use the
# generic subclass of Fault.
end
end
me.subclass ||= Fault
return me
end
end
class RequestFormat < HasDocs
include RepresentationContainer
in_document 'request'
has_many RepresentationFormat
has_many Param
# Returns a URI and a set of HTTP headers for this request.
def uri(resource, args={})
uri = resource.uri(args)
query_values = args[:query] || {}
header_values = args[:headers] || {}
params.each do |param|
if param.style == 'header'
value = header_values[param.name] || header_values[param.name.to_sym]
value = param % value
uri.headers[param.name] = value if value
else
value = query_values[param.name] || query_values[param.name.to_sym]
value = param.%(value, nil, 'query')
uri.query << value if value
end
end
return uri
end
end
class ResponseFormat < HasDocs
include RepresentationContainer
in_document 'response'
has_many RepresentationFormat, FaultFormat
# Builds a service response object out of an HTTPResponse object.
def build(http_response)
# Figure out which fault or representation to use.
status = http_response.status[0]
response_format = self.faults.detect do |f|
f.dereference.status == status
end
unless response_format
# Try to match the response to a response format using a media
# type.
response_media_type = http_response.content_type
response_format = representations.detect do |f|
t = f.dereference.mediaType
t && response_media_type.index(t) == 0
end
# If an exact media type match fails, use the mime-types gem to
# match the response to a response format using the underlying
# subtype. This will match "application/xml" with "text/xml".
if !response_format && MIME_TYPES_SUPPORTED
mime_type = MIME::Types[response_media_type]
raw_sub_type = mime_type[0].raw_sub_type if mime_type
response_format = representations.detect do |f|
t = f.dereference.mediaType
if t
response_mime_type = MIME::Types[t]
response_raw_sub_type = response_mime_type[0].raw_sub_type if response_mime_type
response_raw_sub_type == raw_sub_type
end
end
end
# If all else fails, try to find a response that specifies no
# media type. TODO: check if this would be valid WADL.
if !response_format
response_format = representations.detect do |f|
!f.dereference.mediaType
end
end
end
body = http_response.read
if response_format && response_format.mediaType =~ /xml/
begin
body = REXML::Document.new(body)
# Find the appropriate element of the document
if response_format.element
#TODO: don't strip the damn namespace. I'm not very good at
#namespaces and I don't see how to deal with them here.
element = response_format.element.gsub(/.*:/, '')
body = REXML::XPath.first(body, "//#{element}")
end
rescue REXML::ParseException
end
body.extend(XMLRepresentation)
body.representation_of(response_format)
end
clazz = response_format.is_a?(FaultFormat) ? response_format.subclass : Response
obj = clazz.new(http_response.status, http_response, body, response_format)
raise obj if obj.is_a? Exception
return obj
end
end
class HTTPMethod < HasDocs
in_document 'method'
as_collection 'http_methods'
may_be_reference
has_required :id, :name
has_one RequestFormat
has_one ResponseFormat
# Args:
# :path - Values for path parameters
# :query - Values for query parameters
# :headers - Values for header parameters
# :send_representation
# :expect_representation
def call(resource, args={})
unless parent.respond_to? :uri
raise Exception, \
"You can't call a method that's not attached to a resource! (You may have dereferenced a method when you shouldn't have)"
end
resource ||= parent
method = self.dereference
if method.request
uri = method.request.uri(resource, args)
else
uri = resource.uri
end
headers = uri.headers.dup
if args[:expect_representation]
headers['Accept'] = expect_representation.mediaType
end
headers['User-Agent'] = 'Ruby WADL client' unless headers['User-Agent']
headers[:method] = name.downcase.to_sym
headers[:body] = args[:send_representation]
#puts "#{headers[:method].to_s.upcase} #{uri}"
#puts " Options: #{headers.inspect}"
begin
response = open(uri, headers)
rescue OpenURI::HTTPError => e
response = e.io
end
return method.response.build(response)
end
end
# A mixin for objects that contain resources. If you include this, be
# sure to alias :find_resource to :find_resource_autogenerated
# beforehand.
module ResourceContainer
def resource(name_or_id)
name_or_id = name_or_id.to_s
find_resource(nil, Proc.new do |r|
r.id == name_or_id || r.path == name_or_id
end)
end
def find_resource_by_path(path, *args)
path = path.to_s
match_predicate = Proc.new { |resource| resource.path == path }
find_resource(nil, match_predicate, *args)
end
def finalize_creation
return unless resources
resources.each do |r|
if r.id && !r.respond_to?(r.id)
define_singleton(r.id, "find_resource('#{r.id}')")
end
end
resources.each do |r|
if r.path && !r.respond_to?(r.path)
define_singleton(r.path, "find_resource_by_path('#{r.path}')")
end
end
end
end
# A type of resource. Basically a mixin of methods and params for actual
# resources.
class ResourceType < HasDocs
in_document 'resource_type'
as_collection 'resource_types'
has_many HTTPMethod
has_many Param
has_attributes :id
end
class Resource < HasDocs
in_document 'resource'
as_collection 'resources'
has_many Resource
has_many HTTPMethod
has_many Param
has_many ResourceType
has_attributes :id, :path
include ResourceContainer
def initialize(*args)
super(*args)
end
def dereference_with_context(child)
ResourceAndAddress.new(child, parent.address)
end
# Returns a ResourceAndAddress object bound to this resource
# and the given query variables.
def bind(args={})
resource = ResourceAndAddress.new(self)
resource.bind!(args)
return resource
end
# Sets basic auth parameters
def with_basic_auth(user, pass, param_name='Authorization')
value = 'Basic ' + [user.to_s+':'+pass.to_s].pack('m')
a = bind(:headers => {param_name => value })
end
def uri(args={}, working_address=nil)
working_address = working_address.deep_copy if working_address
address(working_address).uri(args)
end
# Returns an Address object refering to this resource
def address(working_address=nil)
if working_address
working_address = working_address.deep_copy
else
if parent.respond_to? :base
working_address = Address.new()
working_address.path_fragments << parent.base
else
working_address = parent.address.deep_copy
end
end
working_address.path_fragments << path.dup
# Install path, query, and header parameters in the Address. These
# may override existing parameters with the same names, but if
# you've got a WADL application that works that way, you should
# have bound parameters to values earlier.
new_path_fragments = []
embedded_param_names = Set.new(Address.embedded_param_names(path))
params.each do |param|
if embedded_param_names.member? param.name
working_address.path_params[param.name] = param
else
if param.style == 'query'
working_address.query_params[param.name] = param
elsif param.style == 'header'
working_address.header_params[param.name] = param
else
new_path_fragments << param
working_address.path_params[param.name] = param
end
end
end
working_address.path_fragments << new_path_fragments unless new_path_fragments.empty?
return working_address
end
def representation_for(http_method, request=true, all=false)
method = find_method_by_http_method(http_method)
if request
container = method.request
else
container = method.response
end
representations = container.representations
unless all
representations = representations[0]
end
return representations
end
def find_by_id(id)
id = id.to_s
resources.detect { |r| r.dereference.id == id }
end
# Find HTTP methods in this resource and in the mixed-in types
def each_http_method
http_methods.each { |m| yield m }
resource_types.each do |t|
t.http_methods.each { |m| yield m }
end
end
def find_method_by_id(id)
id = id.to_s
each_http_method { |m| return m if m.dereference.id == id }
end
def find_method_by_http_method(action)
action = action.to_s.downcase
each_http_method { |m| return m if m.dereference.name.downcase == action }
end
# Methods for reading or writing this resource
def get(*args, &block)
find_method_by_http_method('get').call(self, *args, &block)
end
def post(*args, &block)
find_method_by_http_method('post').call(self, *args, &block)
end
def put(*args, &block)
find_method_by_http_method('put').call(self, *args, &block)
end
def delete(*args, &block)
find_method_by_http_method('delete').call(self, *args, &block)
end
end
# A resource bound beneath a certain address. Used to keep track of a
# path through a twisting resource hierarchy that includes references.
class ResourceAndAddress < DelegateClass(Resource)
def initialize(resource, address=nil, combine_address_with_resource=true)
@resource = resource
if combine_address_with_resource
@address = @resource.address(address)
else
@address = address
end
super(resource)
end
# The id method is not delegated, because it's the name of a
# (deprecated) built-in Ruby method. We wnat to delegate it.
def id
@resource.id
end
def to_s
inspect
end
def inspect
"ResourceAndAddress\n Resource: #{@resource.to_s}\n #{@address.inspect}"
end
def address
@address
end
def bind(*args)
ResourceAndAddress.new(@resource, @address.deep_copy, false).bind!(*args)
end
def bind!(args={})
@address.bind!(args)
self
end
def uri(args={})
@address.deep_copy.bind!(args).uri
end
# method_missing is to catch generated methods that don't get delegated.
def method_missing(name, *args, &block)
if @resource.respond_to? name
result = @resource.send(name, *args, &block)
if result.is_a? Resource
result = ResourceAndAddress.new(result, @address.dup)
end
return result
else
raise NoMethodError, "undefined method `#{name}' for #{self}:#{self.class}"
end
end
# method_missing won't catch these guys because they were defined in
# the delegation operation.
def resource(*args, &block)
resource = @resource.resource(*args, &block)
resource ? ResourceAndAddress.new(resource, @address) : resource
end
def find_resource(*args, &block)
resource = @resource.find_resource(*args, &block)
resource ? ResourceAndAddress.new(resource, @address) : resource
end
def find_resource_by_path(*args, &block)
resource = @resource.find_resource_by_path(*args, &block)
resource ? ResourceAndAddress.new(resource, @address) : resource
end
def get(*args, &block)
find_method_by_http_method('get').call(self, *args, &block)
end
def post(*args, &block)
find_method_by_http_method('post').call(self, *args, &block)
end
def put(*args, &block)
find_method_by_http_method('put').call(self, *args, &block)
end
def delete(*args, &block)
find_method_by_http_method('delete').call(self, *args, &block)
end
end
class Resources < HasDocs
in_document 'resources'
as_member 'resource_list'
has_many Resource
has_attributes :base
include ResourceContainer
end
class Application < HasDocs
in_document 'application'
has_one Resources
has_many HTTPMethod, RepresentationFormat, FaultFormat
def Application.from_wadl(wadl)
wadl = wadl.read if wadl.respond_to?(:read)
doc = REXML::Document.new(wadl)
need_finalization = []
application = from_element(nil, doc.root, need_finalization)
need_finalization.each { |x| x.finalize_creation }
return application
end
def find_resource(symbol, *args, &block)
resource_list.find_resource(symbol, *args, &block)
end
def resource(symbol)
resource_list.resource(symbol)
end
def find_resource_by_path(symbol, *args, &block)
resource_list.find_resource_by_path(symbol, *args, &block)
end
def finalize_creation
return unless resource_list
resource_list.resources.each do |r|
if r.id && !r.respond_to?(r.id)
define_singleton(r.id, "resource_list.find_resource('#{r.id}')")
end
end
resource_list.resources.each do |r|
if r.path && !r.respond_to?(r.path)
define_singleton(r.path,
"resource_list.find_resource_by_path('#{r.path}')")
end
end
end
end
# A module mixed in to REXML documents to make them representations in the
# WADL sense.
module XMLRepresentation
def representation_of(format)
@params = format.params
end
def lookup_param(name)
p = @params.detect { |p| p.name = name }
raise ArgumentError, "No such param #{name}" unless p
raise ArgumentError, "Param #{name} has no path!" unless p.path
return p
end
# Yields up each XML element for the given Param object.
def each_by_param(param_name)
REXML::XPath.each(self, lookup_param(param_name).path) { |e| yield e }
end
# Returns an XML element for the given Param object.
def get_by_param(param_name)
REXML::XPath.first(self, lookup_param(param_name).path)
end
end
Response = Struct.new(:code, :headers, :representation, :format)
class Fault < Exception
attr_accessor :code, :headers, :representation, :format
def initialize(code, headers, representation, format)
self.code = code
self.headers = headers
self.representation = representation
self.format = format
end
end
end # End WADL module