# 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
Published in: Ruby
Download

Related snippets