SHARE
TWEET

Sam Joseph

a guest Sep 14th, 2010 83 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. # wadl.rb
  2. # http://www.crummy.com/software/wadl.rb/
  3. # Super cheap Ruby WADL client
  4. # by Leonard Richardson leonardr@segfault.org
  5. # v20070217
  6. # For more on WADL, see http://wadl.dev.java.net/
  7.  
  8. require 'rubygems'
  9. require 'rest-open-uri'
  10.  
  11. require 'delegate'
  12. require 'rexml/document'
  13. require 'set'
  14. require 'cgi'
  15.  
  16. begin
  17.   require 'rubygems'
  18.   require 'mime/types'
  19.   MIME_TYPES_SUPPORTED = true
  20. rescue LoadError
  21.   MIME_TYPES_SUPPORTED = false
  22. end
  23.  
  24. module WADL
  25.  
  26. # A container for application-specific faults
  27. module Faults
  28. end
  29.  
  30. #########################################################################
  31. #
  32. # A cheap way of defining an XML schema as Ruby classes and then parsing
  33. # documents into instances of those classes.
  34. class CheapSchema
  35.  
  36.   attr_accessor :index_key, :href
  37.  
  38.   @may_be_reference = false
  39.   @contents_are_mixed_data = false
  40.  
  41.   def initialize
  42.     @attributes = {}
  43.     @contents = nil
  44.   end
  45.  
  46.   def self.init
  47.     @names = {}
  48.  
  49.     @members = {}
  50.     @collections = {}
  51.     @required_attributes = []
  52.     @attributes = []
  53.   end
  54.  
  55.   def self.inherit(from)
  56.     init
  57.     @names = from.names.dup if from.names
  58.     @members = from.members.dup if from.members
  59.     @collections = from.collections.dup if from.collections
  60.     @required_attributes = from.required_attributes.dup if from.required_attributes
  61.     @attributes = from.attributes.dup if from.attributes
  62.   end
  63.  
  64.   def self.inherited(klass)    
  65.     klass.inherit(self)
  66.   end
  67.  
  68.   def self.names
  69.     @names
  70.   end
  71.  
  72.   def self.members
  73.     @members
  74.   end
  75.  
  76.   def self.collections
  77.     @collections
  78.   end
  79.  
  80.   def self.required_attributes
  81.     @required_attributes
  82.   end
  83.  
  84.   def self.attributes
  85.     @attributes
  86.   end
  87.  
  88.   def attributes
  89.     @attributes
  90.   end
  91.  
  92.   def self.may_be_reference?
  93.     @may_be_reference
  94.   end
  95.  
  96.   def self.in_document(element_name)
  97.     @names[:element] = element_name
  98.     @names[:member] = element_name
  99.     @names[:collection] = element_name + 's'
  100.   end
  101.  
  102.   def self.as_collection(collection_name)
  103.     @names[:collection] = collection_name
  104.   end
  105.  
  106.   def self.as_member(member_name)
  107.     @names[:member] = member_name
  108.   end
  109.  
  110.   def self.contents_are_mixed_data
  111.     @contents_are_mixed_data = true
  112.   end
  113.  
  114.   def self.has_one(*classes)
  115.     classes.each do |c|
  116.       @members[c.names[:element]] = c
  117.       member_name = c.names[:member]
  118.       dereferencing_instance_accessor member_name
  119.     end
  120.   end
  121.  
  122.   def self.has_many(*classes)
  123.     classes.each do |c|
  124.       @collections[c.names[:element]] = c
  125.       collection_name = c.names[:collection]
  126.       dereferencing_instance_accessor collection_name
  127.       find_method_name = "find_#{c.names[:element]}"      
  128.  
  129.       # Define a method for finding a specific element of this
  130.       # collection.
  131.       # TODO: In Ruby 1.9, make match_block a block argument.
  132.       define_method(find_method_name) do |name, *args|
  133.         name = name.to_s
  134.  
  135.         if args[0].respond_to? :call
  136.           match_block = args[0]
  137.         else
  138.           match_block = Proc.new { |m| m.name_matches(name) }
  139.         end
  140.  
  141.         unless args[1].nil?
  142.           auto_dereference = args[1]
  143.         else
  144.           auto_dereference = true
  145.         end
  146.  
  147.         match = self.send(collection_name).detect do |m|
  148.           match_block.call(m) || \
  149.           (c.may_be_reference? && auto_dereference &&
  150.            match_block.call(m.dereference))
  151.         end
  152.         match = match.dereference if match && auto_dereference
  153.         return match
  154.       end
  155.     end
  156.   end
  157.  
  158.   def self.dereferencing_instance_accessor(*symbols)
  159.     symbols.each do |name|
  160.       define_method(name) do
  161.         dereference.instance_variable_get("@#{name}")
  162.       end
  163.       define_method(name.to_s+'=') do |value|
  164.         dereference.instance_variable_set("@#{name}", value)
  165.       end
  166.     end
  167.   end
  168.  
  169.   def self.dereferencing_attr_accessor(*symbols)
  170.     symbols.each do |name|
  171.       m = instance_methods
  172.       define_method(name) do
  173.         dereference.attributes[name.to_s]
  174.       end
  175.       define_method(name.to_s+'=') do |value|
  176.         dereference.attributes[name.to_s] = value
  177.       end
  178.     end
  179.   end
  180.  
  181.   def self.has_attributes(*names)
  182.     names.each do |name|
  183.       @attributes << name
  184.       @index_attribute ||= name.to_s
  185.       if name == :href
  186.         attr_accessor name
  187.       else
  188.         dereferencing_attr_accessor name
  189.       end
  190.     end
  191.   end
  192.  
  193.   def self.has_required(*names)
  194.     names.each do |name|
  195.       @required_attributes << name
  196.       @index_attribute ||= name.to_s
  197.       if name == :href
  198.         attr_accessor name
  199.       else
  200.         dereferencing_attr_accessor name
  201.       end
  202.     end
  203.   end
  204.  
  205.   def self.may_be_reference
  206.     @may_be_reference = true
  207.     define_method("dereference") do
  208.       return self if not self.attributes['href']
  209.       unless @referenced      
  210.         if self.attributes['href']
  211.           find_method_name = "find_#{self.class.names[:element]}"          
  212.           p = self
  213.           until @referenced or !p do
  214.             begin
  215.               p = p.parent
  216.             end until !p or p.respond_to? find_method_name
  217.             if p
  218.               @referenced = p.send(find_method_name, self.attributes['href'], nil, false) if p
  219.             else
  220.               @referenced = nil
  221.             end
  222.           end
  223.         end
  224.       end
  225.       @referenced ? dereference_with_context(@referenced) : nil
  226.     end
  227.   end
  228.  
  229.   # This object is a reference to another object. This method returns
  230.   # an object that acts like the other object, but also contains any
  231.   # neccessary context about this object. See the ResourceAndAddress
  232.   # implementation, in which a dereferenced resource contains
  233.   # information about the parent of the resource that referenced it
  234.   # (otherwise, there's no way to build the URI).
  235.   def dereference_with_context(referent)
  236.     referent
  237.   end
  238.  
  239.   # Turn an XML element into an instance of this class.
  240.   def self.from_element(parent, e, need_finalization)
  241.     attributes = e.attributes
  242.     me = self.new
  243.     me.parent = parent
  244.    
  245.     @collections.each do |name, clazz|
  246.       collection_name = "@" + clazz.names[:collection].to_s
  247.       me.instance_variable_set(collection_name, [])
  248.     end
  249.      
  250.     if @may_be_reference and attributes['href']
  251.       # Handle objects that are just references to other objects
  252.       # somewhere above this one in the hierarchy
  253.       href = attributes['href']
  254.       if href[0] == ?#
  255.         href = href[1..href.size]
  256.       else
  257.         puts "Warning: HREF #{href} should be ##{href}"
  258.       end
  259.       me.attributes['href'] = href
  260.  
  261.     else
  262.       # Handle this element's attributes
  263.       @required_attributes.each do |name|
  264.         name = name.to_s
  265.         unless attributes[name]
  266.           raise ArgumentError, %{Missing required attribute "#{name}" in element: #{e}}
  267.         end
  268.         #puts " #{name}=#{attributes[name]}"
  269.         me.attributes[name.to_s] = attributes[name]
  270.         me.index_key = attributes[name] if name == @index_attribute
  271.       end
  272.      
  273.       @attributes.each do |name|
  274.         name = name.to_s
  275.         #puts " #{name}=#{attributes[name]}"
  276.         me.attributes[name.to_s] = attributes[name]
  277.         me.index_key = attributes[name] if name == @index_attribute
  278.       end      
  279.     end
  280.  
  281.     # Handle this element's children.
  282.     if @contents_are_mixed_data
  283.       me.instance_variable_set('@contents', e.children)
  284.     else
  285.       e.each_element do |child|
  286.         clazz = @members[child.name] || @collections[child.name]
  287.         if clazz        
  288.           object = clazz.from_element(me, child, need_finalization)
  289.           if clazz == @members[child.name]
  290.             #puts "#{self.name} can have one #{clazz.name}"
  291.             instance_variable_name = "@" + clazz.names[:member].to_s
  292.             if me.instance_variable_get(instance_variable_name)
  293.               raise "#{self.name} can only have one #{clazz.name}, but several were specified in element: #{e}"
  294.             end          
  295.             #puts "Setting its #{instance_variable_name} to a #{object.class.name}"
  296.             me.instance_variable_set(instance_variable_name, object)
  297.           else
  298.             #puts "#{self.name} can have many #{clazz.name}"
  299.             collection_name = "@" + clazz.names[:collection].to_s
  300.             collection = me.instance_variable_get(collection_name)
  301.             #puts "Adding a #{object.class.name} to #{collection_name} collection"
  302.             collection << object
  303.           end
  304.         end
  305.       end
  306.     end
  307.     need_finalization << me if me.respond_to? :finalize_creation
  308.     return me
  309.   end
  310.  
  311.   # Common instance methods
  312.  
  313.   attr_accessor :parent
  314.  
  315.   # A null implementation so that foo.dereference will always return the
  316.   # "real" object.
  317.   def dereference
  318.     self
  319.   end
  320.  
  321.   # Returns whether or not the given name matches this object.
  322.   # By default, checks the index key for this class.
  323.   def name_matches(name)
  324.     index_key == name    
  325.   end
  326.  
  327.   def to_s(indent=0)
  328.     s = ""
  329.     i = " " * indent
  330.     s << "#{i}#{self.class.name}\n"
  331.     if self.class.may_be_reference? and self.attributes['href']
  332.       s << "#{i} href=#{self.attributes['href']}\n"
  333.     else
  334.       [self.class.required_attributes, self.class.attributes].each do |list|
  335.         list.each do |attr|
  336.           attr = attr.to_s
  337.           s << "#{i} #{attr}=#{self.attributes[attr]}\n" if self.attributes[attr]
  338.         end
  339.       end
  340.       self.class.members.each_value do |member_class|
  341.         o = self.send(member_class.names[:member])
  342.         s << o.to_s(indent+1) if o
  343.       end
  344.       self.class.collections.each_value do |collection_class|
  345.         c = self.send(collection_class.names[:collection])
  346.         if c and not c.empty?
  347.           s << "#{i} Collection of #{c.size} #{collection_class.name}(s)\n"
  348.           c.each do |o|
  349.             s << o.to_s(indent+2)
  350.           end
  351.         end
  352.       end
  353.       if @contents && !@contents.empty?
  354.         s << '-' * 80 << "\n" << @contents.join(' ') << "\n" << '-' * 80 << "\n"
  355.       end
  356.     end
  357.     return s
  358.   end
  359. end
  360.  
  361. #########################################################################
  362. # Classes to keep track of the logical structure of a URI.
  363.  
  364. URIParts = Struct.new(:uri, :query, :headers)
  365. class URIParts
  366.  
  367.   def to_s
  368.     u = uri.dup
  369.     unless query.empty?
  370.       u << (uri.index('?') ? '&' : '?')
  371.       u << query_string
  372.     end
  373.     u
  374.   end
  375.  
  376.   def inspect
  377.     s = to_s
  378.     s << " Plus headers: #{headers.inspect}" if headers
  379.   end
  380.  
  381.   def query_string
  382.     query.join('&')
  383.   end
  384.  
  385.   def hash(x)
  386.     to_str.hash
  387.   end
  388.  
  389.   def ==(x)
  390.     return to_str == x if x.respond_to? :to_str
  391.     return super
  392.   end
  393.  
  394.   alias :to_str :to_s
  395. end
  396.  
  397. # The Address class keeps track of the user's path through a resource
  398. # graph. Values for WADL parameters may be specified at any time using
  399. # the bind method. An Address cannot be turned into a URI and header
  400. # set until all required parameters have been bound to values.
  401. #
  402. # An Address object is built up through calls to Resource#address
  403. class Address
  404.   attr_reader :path_fragments, :query_vars, :headers, \
  405.               :path_params, :query_params, :header_params
  406.  
  407.   def initialize(path_fragments=[], query_vars=[], headers={},
  408.                  path_params={}, query_params={}, header_params={})
  409.     @path_fragments = path_fragments
  410.     @query_vars, @headers = query_vars, headers
  411.  
  412.     @path_params, @query_params, @header_params = path_params, query_params, header_params
  413.   end
  414.  
  415.   def _deep_copy_hash(h)
  416.     a = h.inject({}) { |h,kv| h[kv[0]] = (kv[1] ? kv[1].dup : kv[1]); h }
  417.   end
  418.  
  419.   def _deep_copy_array(a)
  420.     a.inject([]) { |a,e| a << (e ? e.dup : e) }
  421.   end
  422.  
  423.   # Perform a deep copy.
  424.   def deep_copy
  425.     Address.new(_deep_copy_array(@path_fragments),
  426.                 _deep_copy_array(@query_vars), _deep_copy_hash(@headers),
  427.                 @path_params.dup, @query_params.dup,
  428.                 @header_params.dup)
  429.   end
  430.  
  431.   def to_s
  432.     s = "Address:\n"
  433.     s << " Path fragments: #{@path_fragments.inspect}\n"
  434.     s << " Query variables: #{@query_vars.inspect}\n"
  435.     s << " Header variables: #{@headers.inspect}\n"
  436.     s << " Unbound path parameters: #{@path_params.inspect}\n"
  437.     s << " Unbound query parameters: #{@query_params.inspect}\n"
  438.     s << " Unbound header parameters: #{@header_params.inspect}\n"
  439.   end
  440.  
  441.   alias :inspect :to_s
  442.  
  443.   def self.embedded_param_names(fragment)
  444.     fragment.scan(/\{([^}]+)\}/).flatten
  445.   end
  446.  
  447.   # Binds some or all of the unbound variables in this address to values.
  448.   def bind!(args={})
  449.     path_var_values = args[:path] || {}
  450.     query_var_values = args[:query] || {}
  451.     header_var_values = args[:headers] || {}
  452.     # Bind variables found in the path fragments.
  453.     if path_var_values
  454.       path_params_to_delete = []
  455.       path_fragments.each do |fragment|
  456.         if fragment.respond_to? :to_str
  457.           # This fragment is a string which might contain {} substitutions.
  458.           # Make any substitutions available to the provided path variables.
  459.           embedded_param_names = self.class.embedded_param_names(fragment)
  460.           embedded_param_names.each do |param_name|
  461.             value = path_var_values[param_name] || path_var_values[param_name.to_sym]
  462.             param = path_params[param_name]
  463.             if param
  464.               value = param % value
  465.               path_params_to_delete << param
  466.             else
  467.               value = Param.default.%(value, param_name)
  468.             end
  469.             fragment.gsub!('{' + param_name + '}', value)
  470.           end
  471.         else
  472.           # This fragment is an array of Param objects (style 'matrix'
  473.           # or 'plain') which may be bound to strings. As substitutions
  474.           # happen, the array will become a mixed array of Param objects
  475.           # and strings.
  476.           fragment.each_with_index do |param, i|
  477.             if param.respond_to? :name
  478.               value = path_var_values[param.name] || path_var_values[param.name.to_sym]
  479.               new_value = param % value          
  480.               fragment[i] = new_value if new_value
  481.               path_params_to_delete << param
  482.             end
  483.           end
  484.         end
  485.       end
  486.  
  487.       # Delete any embedded path parameters that are now bound from
  488.       # our list of unbound parameters.
  489.       path_params_to_delete.each { |p| path_params.delete(p.name) }      
  490.     end
  491.  
  492.     # Bind query variable values to query parameters
  493.     query_var_values.each do |name, value|
  494.       param = query_params[name.to_s]
  495.       if param
  496.         query_vars << param % value
  497.         query_params.delete(name.to_s)
  498.       end
  499.     end
  500.  
  501.     # Bind header variables to header parameters
  502.     header_var_values.each do |name, value|
  503.       param = header_params[name.to_s]
  504.       if param
  505.         headers[name] = param % value
  506.         header_params.delete(name.to_s)
  507.       end
  508.     end
  509.     return self
  510.   end
  511.  
  512.   def uri(args={})
  513.     obj = deep_copy
  514.     obj.bind!(args)
  515.  
  516.     # Build the path
  517.     uri = ''
  518.     obj.path_fragments.flatten.each do |fragment|
  519.       if fragment.respond_to? :to_str
  520.         embedded_param_names = self.class.embedded_param_names(fragment)
  521.         unless embedded_param_names.empty?
  522.           raise ArgumentError, %{Missing a value for required path parameter "#{embedded_param_names[0]}"!}
  523.         end
  524.         unless fragment.empty?
  525.           uri << '/' if !uri.empty? && uri[-1] != ?/
  526.           uri << fragment
  527.         end
  528.       elsif fragment.required
  529.         # This is a required Param that was never bound to a value.
  530.         raise ArgumentError, %{Missing a value for required path parameter "#{fragment.name}"!}
  531.       end
  532.     end
  533.  
  534.     # Hunt for required unbound query parameters.
  535.     obj.query_params.each do |name, value|
  536.       if value.required
  537.         raise ArgumentError, %{Missing a value for required query parameter "#{value.name}"!}
  538.       end
  539.     end
  540.  
  541.     # Hunt for required unbound header parameters.
  542.     obj.header_params.each do |name, value|
  543.       if value.required
  544.         raise ArgumentError, %{Missing a value for required header parameter "#{value.name}"!}
  545.       end
  546.     end
  547.  
  548.     return URIParts.new(uri, obj.query_vars, obj.headers)
  549.   end
  550.  
  551. end
  552.  
  553. #########################################################################
  554. #
  555. # Now we use Ruby classes to define the structure of a WADL document
  556.  
  557. class Documentation < CheapSchema
  558.   in_document 'doc'
  559.   as_member 'doc'
  560.   as_collection 'docs'
  561.  
  562.   has_attributes "xml:lang", :title
  563.   contents_are_mixed_data
  564. end
  565.  
  566. class HasDocs < CheapSchema
  567.   has_many Documentation
  568.  
  569.   # Convenience method to define a no-argument singleton method on
  570.   # this object.
  571.   def define_singleton(name, contents)
  572.     return if name =~ /[^A-Za-z0-9_]/
  573.     instance_eval(%{def #{name}
  574.                       #{contents}
  575.                     end})
  576.   end
  577. end
  578.  
  579. class Option < HasDocs
  580.   in_document 'option'
  581.   as_member 'option'
  582.   as_collection 'options'
  583.   has_required :value
  584. end
  585.  
  586. class Link < HasDocs
  587.   in_document 'link'
  588.   as_member 'link'
  589.   as_collection 'links'
  590.   has_attributes :href, :rel, :rev
  591. end
  592.  
  593. class Param < HasDocs
  594.   in_document 'param'
  595.   as_member 'param'
  596.   as_collection 'params'
  597.   has_required :name
  598.   has_attributes :type, :default, :style, :path, :required, :repeating, :fixed
  599.   has_many Option
  600.   has_many Link
  601.  
  602.   def inspect
  603.     %{Param "#{name}"}
  604.   end
  605.  
  606.   # Validates and formats a proposed value for this parameter. Returns
  607.   # the formatted value. Raises an ArgumentError if the value
  608.   # is invalid.
  609.   #
  610.   # The 'name' and 'style' arguments are used in conjunction with the
  611.   # default Param object.
  612.   def %(value, name=nil, style=nil)
  613.     name ||= self.name
  614.     style ||= self.style
  615.     value = fixed if fixed
  616.     unless value
  617.       if default
  618.         value = default
  619.       elsif required
  620.         raise ArgumentError, "No value provided for required param \"#{name}\"!"
  621.       else
  622.         return '' # No value provided and none required.
  623.       end
  624.     end
  625.  
  626.     if value.respond_to?(:each) && !value.respond_to?(:to_str)
  627.       if repeating
  628.         values = value
  629.       else
  630.         raise ArgumentError, "Multiple values provided for single-value param \"#{name}\""
  631.       end
  632.     else
  633.       values = [value]
  634.     end
  635.      
  636.     # If the param lists acceptable values in option tags, make sure that
  637.     # all values are found in those tags.
  638.     if options && !options.empty?
  639.       values.each do |value|
  640.         unless find_option(value)
  641.           acceptable = options.collect { |o| o.value }.join('", "')
  642.             raise ArgumentError, %{"#{value}" is not among the acceptable parameter values ("#{acceptable}")}
  643.         end
  644.       end
  645.     end
  646.  
  647.     if style == 'query' || parent.is_a?(RequestFormat) ||
  648.         (parent.respond_to?('is_form_representation?') \
  649.           && parent.is_form_representation?)
  650.       value = values.collect do |v|
  651.         URI.escape(name) + '=' + URI.escape(v.to_s)
  652.       end.join('&')
  653.     elsif self.style == 'matrix'      
  654.       if type == 'xsd:boolean'
  655.         value = values.collect { |v| (v == 'true' || v == true) ? ';' + name : '' }.join('')
  656.       else
  657.         value = values.collect do |v|
  658.           v ? ';' + URI.escape(name) + '=' + URI.escape(v.to_s) : ''
  659.         end.join('')
  660.       end
  661.     elsif self.style == 'header'
  662.       value = values.join(',')
  663.     else
  664.       # All other cases: plain text representation.
  665.       value = values.collect { |v| URI.escape(v.to_s) }.join(',')
  666.     end
  667.     return value
  668.   end
  669.  
  670.  
  671.   # A default Param object to use for a path parameter that is
  672.   # only specified as a name in the path of a resource.
  673.   @@default = Param.new
  674.   @@default.required = true
  675.   @@default.style = 'plain'
  676.   @@default.type = 'xsd:string'
  677.  
  678.   def self.default
  679.     @@default
  680.   end
  681. end
  682.  
  683. # A mixin for objects that contain representations
  684. module RepresentationContainer
  685.   def find_representation_by_media_type(type)
  686.     representations.detect { |r| r.mediaType == type }
  687.   end
  688.  
  689.   def find_form
  690.     representations.detect { |r| r.is_form_representation? }
  691.   end
  692. end
  693.  
  694. class RepresentationFormat < HasDocs
  695.   in_document 'representation'
  696.   as_collection 'representations'
  697.   may_be_reference
  698.   has_attributes :id, :mediaType, :element
  699.   has_many Param
  700.  
  701.   def is_form_representation?
  702.     return mediaType == 'application/x-www-form-encoded' ||
  703.       mediaType == 'multipart/form-data'
  704.   end
  705.  
  706.   # Creates a representation by plugging a set of parameters
  707.   # into a representation format.
  708.   def %(values)
  709.     if mediaType == 'application/x-www-form-encoded'
  710.       representation = []
  711.       params.each do |param|
  712.         if param.fixed
  713.           p_values = [param.fixed]
  714.         elsif values[param.name] || values[param.name.to_sym]
  715.           p_values = values[param.name] || values[param.name.to_sym]
  716.           if !param.repeating || !(p_values.respond_to?(:each) && !p_values.respond_to?(:to_str))
  717.             p_values = [p_values]
  718.           end
  719.         else
  720.           if param.required
  721.             raise ArgumentError,  "Your proposed representation is missing a value for #{param.name}"
  722.           end
  723.         end
  724.         if p_values
  725.           p_values.each do |value|
  726.             representation << CGI::escape(param.name) + '=' + CGI::escape(value.to_s)
  727.           end
  728.         end
  729.       end
  730.       representation = representation.join('&')
  731.     else
  732.       raise Exception,
  733.       "wadl.rb can't instantiate a representation of type #{mediaType}"
  734.     end
  735.     return representation
  736.   end
  737. end
  738.  
  739. class FaultFormat < RepresentationFormat
  740.   in_document 'fault'
  741.   as_collection 'faults'
  742.   may_be_reference
  743.   has_attributes :id, :mediaType, :element, :status
  744.   has_many Param
  745.  
  746.   attr_writer :subclass
  747.  
  748.   def subclass
  749.     if attributes['href']
  750.       dereference.subclass
  751.     else
  752.       @subclass
  753.     end
  754.   end
  755.  
  756.   # Define a custom subclass for this fault, so that the programmer
  757.   # can rescue this particular fault.
  758.   def self.from_element(*args)
  759.     me = super
  760.     return me if me.attributes['href']
  761.     name = me.attributes['id']
  762.     if name
  763.       begin
  764.         c = Class.new(Fault)
  765.         WADL::Faults.const_set(name, c) unless WADL::Faults.const_defined? name
  766.         me.subclass = c
  767.       rescue NameError => e
  768.         # This fault format's ID can't be a class name. Use the
  769.         # generic subclass of Fault.
  770.       end
  771.     end
  772.     me.subclass ||= Fault
  773.     return me
  774.   end
  775. end
  776.  
  777. class RequestFormat < HasDocs
  778.   include RepresentationContainer
  779.   in_document 'request'
  780.   has_many RepresentationFormat
  781.   has_many Param
  782.  
  783.   # Returns a URI and a set of HTTP headers for this request.
  784.   def uri(resource, args={})
  785.     uri = resource.uri(args)
  786.     query_values = args[:query] || {}
  787.     header_values = args[:headers] || {}
  788.     params.each do |param|
  789.       if param.style == 'header'
  790.         value = header_values[param.name] || header_values[param.name.to_sym]
  791.         value = param % value
  792.         uri.headers[param.name] = value if value
  793.       else        
  794.         value = query_values[param.name] || query_values[param.name.to_sym]
  795.         value = param.%(value, nil, 'query')
  796.         uri.query << value if value
  797.       end
  798.     end
  799.     return uri
  800.   end
  801. end
  802.  
  803. class ResponseFormat < HasDocs
  804.   include RepresentationContainer
  805.   in_document 'response'
  806.   has_many RepresentationFormat, FaultFormat
  807.  
  808.   # Builds a service response object out of an HTTPResponse object.
  809.   def build(http_response)
  810.     # Figure out which fault or representation to use.
  811.  
  812.     status = http_response.status[0]
  813.     response_format = self.faults.detect do |f|
  814.       f.dereference.status == status
  815.     end
  816.  
  817.     unless response_format
  818.       # Try to match the response to a response format using a media
  819.       # type.      
  820.       response_media_type = http_response.content_type
  821.       response_format = representations.detect do |f|
  822.         t = f.dereference.mediaType
  823.         t && response_media_type.index(t) == 0
  824.       end      
  825.  
  826.       # If an exact media type match fails, use the mime-types gem to
  827.       # match the response to a response format using the underlying
  828.       # subtype. This will match "application/xml" with "text/xml".
  829.       if !response_format && MIME_TYPES_SUPPORTED
  830.         mime_type = MIME::Types[response_media_type]
  831.         raw_sub_type = mime_type[0].raw_sub_type if mime_type
  832.         response_format = representations.detect do |f|
  833.           t = f.dereference.mediaType
  834.           if t
  835.             response_mime_type = MIME::Types[t]
  836.             response_raw_sub_type = response_mime_type[0].raw_sub_type if response_mime_type
  837.             response_raw_sub_type == raw_sub_type
  838.           end
  839.         end
  840.       end
  841.  
  842.       # If all else fails, try to find a response that specifies no
  843.       # media type. TODO: check if this would be valid WADL.
  844.       if !response_format
  845.         response_format = representations.detect do |f|
  846.           !f.dereference.mediaType
  847.         end
  848.       end
  849.     end
  850.  
  851.     body = http_response.read
  852.     if response_format && response_format.mediaType =~ /xml/
  853.       begin
  854.         body = REXML::Document.new(body)
  855.         # Find the appropriate element of the document
  856.         if response_format.element
  857.           #TODO: don't strip the damn namespace. I'm not very good at
  858.           #namespaces and I don't see how to deal with them here.
  859.           element = response_format.element.gsub(/.*:/, '')
  860.           body = REXML::XPath.first(body, "//#{element}")
  861.         end
  862.       rescue REXML::ParseException        
  863.       end
  864.       body.extend(XMLRepresentation)
  865.       body.representation_of(response_format)
  866.     end
  867.  
  868.     clazz = response_format.is_a?(FaultFormat) ? response_format.subclass : Response
  869.     obj = clazz.new(http_response.status, http_response, body, response_format)
  870.     raise obj if obj.is_a? Exception
  871.     return obj
  872.   end
  873. end
  874.  
  875. class HTTPMethod < HasDocs
  876.   in_document 'method'
  877.   as_collection 'http_methods'
  878.   may_be_reference
  879.   has_required :id, :name
  880.   has_one RequestFormat
  881.   has_one ResponseFormat
  882.  
  883.   # Args:
  884.   #  :path - Values for path parameters
  885.   #  :query - Values for query parameters
  886.   #  :headers - Values for header parameters
  887.   #  :send_representation
  888.   #  :expect_representation
  889.   def call(resource, args={})
  890.     unless parent.respond_to? :uri
  891.       raise Exception, \
  892.       "You can't call a method that's not attached to a resource! (You may have dereferenced a method when you shouldn't have)"
  893.     end
  894.     resource ||= parent
  895.  
  896.     method = self.dereference
  897.     if method.request
  898.       uri = method.request.uri(resource, args)
  899.     else
  900.       uri = resource.uri
  901.     end
  902.     headers = uri.headers.dup
  903.     if args[:expect_representation]
  904.       headers['Accept'] = expect_representation.mediaType
  905.     end
  906.     headers['User-Agent'] = 'Ruby WADL client' unless headers['User-Agent']
  907.     headers[:method] = name.downcase.to_sym
  908.     headers[:body] = args[:send_representation]
  909.  
  910.     #puts "#{headers[:method].to_s.upcase} #{uri}"
  911.     #puts " Options: #{headers.inspect}"
  912.     begin
  913.       response = open(uri, headers)
  914.     rescue OpenURI::HTTPError => e
  915.       response = e.io
  916.     end
  917.     return method.response.build(response)
  918.   end
  919. end
  920.  
  921. # A mixin for objects that contain resources. If you include this, be
  922. # sure to alias :find_resource to :find_resource_autogenerated
  923. # beforehand.
  924. module ResourceContainer
  925.   def resource(name_or_id)
  926.     name_or_id = name_or_id.to_s
  927.     find_resource(nil, Proc.new do |r|
  928.                     r.id == name_or_id || r.path == name_or_id
  929.                   end)
  930.   end
  931.  
  932.   def find_resource_by_path(path, *args)
  933.     path = path.to_s
  934.     match_predicate = Proc.new { |resource| resource.path == path }
  935.     find_resource(nil, match_predicate, *args)
  936.   end
  937.  
  938.   def finalize_creation
  939.     return unless resources
  940.     resources.each do |r|  
  941.       if r.id && !r.respond_to?(r.id)
  942.         define_singleton(r.id, "find_resource('#{r.id}')")
  943.       end
  944.     end
  945.  
  946.     resources.each do |r|  
  947.       if r.path && !r.respond_to?(r.path)
  948.         define_singleton(r.path, "find_resource_by_path('#{r.path}')")
  949.       end
  950.     end
  951.   end
  952. end
  953.  
  954. # A type of resource. Basically a mixin of methods and params for actual
  955. # resources.
  956. class ResourceType < HasDocs
  957.   in_document 'resource_type'
  958.   as_collection 'resource_types'
  959.   has_many HTTPMethod
  960.   has_many Param
  961.   has_attributes :id
  962. end
  963.  
  964. class Resource < HasDocs
  965.   in_document 'resource'
  966.   as_collection 'resources'
  967.   has_many Resource
  968.   has_many HTTPMethod
  969.   has_many Param
  970.   has_many ResourceType
  971.   has_attributes :id, :path
  972.  
  973.   include ResourceContainer
  974.  
  975.   def initialize(*args)
  976.     super(*args)
  977.   end
  978.  
  979.   def dereference_with_context(child)
  980.     ResourceAndAddress.new(child, parent.address)
  981.   end
  982.        
  983.   # Returns a ResourceAndAddress object bound to this resource
  984.   # and the given query variables.
  985.   def bind(args={})
  986.     resource = ResourceAndAddress.new(self)
  987.     resource.bind!(args)
  988.     return resource
  989.   end
  990.  
  991.   # Sets basic auth parameters
  992.   def with_basic_auth(user, pass, param_name='Authorization')
  993.     value = 'Basic ' + [user.to_s+':'+pass.to_s].pack('m')
  994.     a = bind(:headers => {param_name => value })
  995.   end
  996.  
  997.   def uri(args={}, working_address=nil)
  998.     working_address = working_address.deep_copy if working_address
  999.     address(working_address).uri(args)
  1000.   end
  1001.  
  1002.   # Returns an Address object refering to this resource
  1003.   def address(working_address=nil)
  1004.     if working_address
  1005.       working_address = working_address.deep_copy
  1006.     else
  1007.       if parent.respond_to? :base
  1008.         working_address = Address.new()
  1009.         working_address.path_fragments << parent.base
  1010.       else
  1011.         working_address = parent.address.deep_copy
  1012.       end
  1013.     end
  1014.     working_address.path_fragments << path.dup
  1015.  
  1016.     # Install path, query, and header parameters in the Address. These
  1017.     # may override existing parameters with the same names, but if
  1018.     # you've got a WADL application that works that way, you should
  1019.     # have bound parameters to values earlier.
  1020.     new_path_fragments = []
  1021.     embedded_param_names = Set.new(Address.embedded_param_names(path))
  1022.     params.each do |param|      
  1023.       if embedded_param_names.member? param.name
  1024.         working_address.path_params[param.name] = param
  1025.       else
  1026.         if param.style == 'query'
  1027.           working_address.query_params[param.name] = param
  1028.         elsif param.style == 'header'
  1029.           working_address.header_params[param.name] = param
  1030.         else
  1031.           new_path_fragments << param
  1032.           working_address.path_params[param.name] = param
  1033.         end
  1034.       end
  1035.     end
  1036.     working_address.path_fragments << new_path_fragments unless new_path_fragments.empty?
  1037.     return working_address
  1038.   end
  1039.  
  1040.   def representation_for(http_method, request=true, all=false)
  1041.     method = find_method_by_http_method(http_method)
  1042.     if request
  1043.       container = method.request
  1044.     else
  1045.       container = method.response
  1046.     end
  1047.     representations = container.representations
  1048.     unless all
  1049.       representations = representations[0]
  1050.     end
  1051.     return representations
  1052.   end
  1053.  
  1054.   def find_by_id(id)
  1055.     id = id.to_s
  1056.     resources.detect { |r| r.dereference.id == id }
  1057.   end
  1058.  
  1059.   # Find HTTP methods in this resource and in the mixed-in types
  1060.   def each_http_method
  1061.     http_methods.each { |m| yield m }
  1062.     resource_types.each do |t|
  1063.       t.http_methods.each { |m| yield m }
  1064.     end
  1065.   end
  1066.  
  1067.   def find_method_by_id(id)
  1068.     id = id.to_s
  1069.     each_http_method { |m| return m if m.dereference.id == id }
  1070.   end
  1071.  
  1072.   def find_method_by_http_method(action)
  1073.     action = action.to_s.downcase
  1074.     each_http_method { |m| return m if m.dereference.name.downcase == action }
  1075.   end
  1076.  
  1077.   # Methods for reading or writing this resource
  1078.  
  1079.   def get(*args, &block)
  1080.     find_method_by_http_method('get').call(self, *args, &block)
  1081.   end
  1082.  
  1083.   def post(*args, &block)
  1084.     find_method_by_http_method('post').call(self, *args, &block)
  1085.   end
  1086.  
  1087.   def put(*args, &block)
  1088.     find_method_by_http_method('put').call(self, *args, &block)
  1089.   end
  1090.  
  1091.   def delete(*args, &block)
  1092.     find_method_by_http_method('delete').call(self, *args, &block)
  1093.   end
  1094. end
  1095.  
  1096. # A resource bound beneath a certain address. Used to keep track of a
  1097. # path through a twisting resource hierarchy that includes references.
  1098. class ResourceAndAddress < DelegateClass(Resource)
  1099.   def initialize(resource, address=nil, combine_address_with_resource=true)
  1100.     @resource = resource
  1101.     if combine_address_with_resource
  1102.       @address = @resource.address(address)
  1103.     else
  1104.       @address = address
  1105.     end
  1106.     super(resource)
  1107.   end
  1108.  
  1109.   # The id method is not delegated, because it's the name of a
  1110.   # (deprecated) built-in Ruby method. We wnat to delegate it.
  1111.   def id
  1112.     @resource.id
  1113.   end
  1114.  
  1115.   def to_s
  1116.     inspect
  1117.   end
  1118.  
  1119.   def inspect
  1120.     "ResourceAndAddress\n Resource: #{@resource.to_s}\n #{@address.inspect}"
  1121.   end
  1122.  
  1123.   def address
  1124.     @address
  1125.   end
  1126.  
  1127.   def bind(*args)
  1128.     ResourceAndAddress.new(@resource, @address.deep_copy, false).bind!(*args)
  1129.   end
  1130.  
  1131.   def bind!(args={})
  1132.     @address.bind!(args)
  1133.     self
  1134.   end
  1135.  
  1136.   def uri(args={})
  1137.     @address.deep_copy.bind!(args).uri
  1138.   end
  1139.  
  1140.   # method_missing is to catch generated methods that don't get delegated.
  1141.   def method_missing(name, *args, &block)
  1142.     if @resource.respond_to? name
  1143.       result = @resource.send(name, *args, &block)
  1144.       if result.is_a? Resource
  1145.         result = ResourceAndAddress.new(result, @address.dup)
  1146.       end
  1147.       return result
  1148.     else
  1149.       raise NoMethodError, "undefined method `#{name}' for #{self}:#{self.class}"
  1150.     end
  1151.   end
  1152.  
  1153.   # method_missing won't catch these guys because they were defined in
  1154.   # the delegation operation.
  1155.   def resource(*args, &block)
  1156.     resource = @resource.resource(*args, &block)
  1157.     resource ? ResourceAndAddress.new(resource, @address) : resource
  1158.   end
  1159.  
  1160.   def find_resource(*args, &block)
  1161.     resource = @resource.find_resource(*args, &block)
  1162.     resource ? ResourceAndAddress.new(resource, @address) : resource
  1163.   end
  1164.  
  1165.   def find_resource_by_path(*args, &block)
  1166.     resource = @resource.find_resource_by_path(*args, &block)
  1167.     resource ? ResourceAndAddress.new(resource, @address) : resource
  1168.   end
  1169.  
  1170.   def get(*args, &block)
  1171.     find_method_by_http_method('get').call(self, *args, &block)
  1172.   end
  1173.  
  1174.   def post(*args, &block)
  1175.     find_method_by_http_method('post').call(self, *args, &block)
  1176.   end
  1177.  
  1178.   def put(*args, &block)
  1179.     find_method_by_http_method('put').call(self, *args, &block)
  1180.   end
  1181.  
  1182.   def delete(*args, &block)
  1183.     find_method_by_http_method('delete').call(self, *args, &block)
  1184.   end
  1185. end
  1186.  
  1187. class Resources < HasDocs
  1188.   in_document 'resources'
  1189.   as_member 'resource_list'
  1190.   has_many Resource
  1191.   has_attributes :base
  1192.  
  1193.   include ResourceContainer
  1194. end
  1195.  
  1196. class Application < HasDocs  
  1197.   in_document 'application'
  1198.   has_one Resources
  1199.   has_many HTTPMethod, RepresentationFormat, FaultFormat
  1200.  
  1201.   def Application.from_wadl(wadl)
  1202.     wadl = wadl.read if wadl.respond_to?(:read)
  1203.     doc = REXML::Document.new(wadl)    
  1204.     need_finalization = []
  1205.     application = from_element(nil, doc.root, need_finalization)
  1206.     need_finalization.each { |x| x.finalize_creation }
  1207.     return application
  1208.   end
  1209.  
  1210.   def find_resource(symbol, *args, &block)
  1211.     resource_list.find_resource(symbol, *args, &block)
  1212.   end
  1213.  
  1214.   def resource(symbol)
  1215.     resource_list.resource(symbol)
  1216.   end
  1217.  
  1218.   def find_resource_by_path(symbol, *args, &block)
  1219.     resource_list.find_resource_by_path(symbol, *args, &block)
  1220.   end
  1221.  
  1222.   def finalize_creation
  1223.     return unless resource_list
  1224.     resource_list.resources.each do |r|  
  1225.       if r.id && !r.respond_to?(r.id)
  1226.         define_singleton(r.id, "resource_list.find_resource('#{r.id}')")
  1227.       end
  1228.     end
  1229.  
  1230.     resource_list.resources.each do |r|  
  1231.       if r.path && !r.respond_to?(r.path)
  1232.         define_singleton(r.path,
  1233.                          "resource_list.find_resource_by_path('#{r.path}')")
  1234.       end
  1235.     end
  1236.   end
  1237. end
  1238.  
  1239. # A module mixed in to REXML documents to make them representations in the
  1240. # WADL sense.
  1241. module XMLRepresentation
  1242.   def representation_of(format)
  1243.     @params = format.params
  1244.   end
  1245.  
  1246.   def lookup_param(name)
  1247.     p = @params.detect { |p| p.name = name }
  1248.     raise ArgumentError, "No such param #{name}" unless p
  1249.     raise ArgumentError, "Param #{name} has no path!" unless p.path
  1250.     return p
  1251.   end
  1252.  
  1253.   # Yields up each XML element for the given Param object.
  1254.   def each_by_param(param_name)  
  1255.     REXML::XPath.each(self, lookup_param(param_name).path) { |e| yield e }
  1256.   end
  1257.  
  1258.   # Returns an XML element for the given Param object.
  1259.   def get_by_param(param_name)
  1260.     REXML::XPath.first(self, lookup_param(param_name).path)
  1261.   end
  1262. end
  1263.  
  1264. Response = Struct.new(:code, :headers, :representation, :format)
  1265. class Fault < Exception
  1266.   attr_accessor :code, :headers, :representation, :format
  1267.   def initialize(code, headers, representation, format)
  1268.     self.code = code
  1269.     self.headers = headers
  1270.     self.representation = representation
  1271.     self.format = format    
  1272.   end
  1273. end
  1274.  
  1275. end # End WADL module
RAW Paste Data
Pastebin PRO Summer Special!
Get 60% OFF on Pastebin PRO accounts!
Top