Created
April 2, 2009 18:47
-
-
Save oddlyzen/89353 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment