You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@deltacloud.apache.org by lu...@apache.org on 2010/07/09 01:35:52 UTC

svn commit: r962197 - in /incubator/deltacloud/trunk/server/libexec: lib/sinatra/ public/stylesheets/compiled/ views/ views/api/ views/docs/

Author: lutter
Date: Thu Jul  8 23:35:51 2010
New Revision: 962197

URL: http://svn.apache.org/viewvc?rev=962197&view=rev
Log:
* lib/sinatra/rabbit.rb: extension for restful routing with validation

Also generates API documentation at /api/docs

Added:
    incubator/deltacloud/trunk/server/libexec/lib/sinatra/rabbit.rb
    incubator/deltacloud/trunk/server/libexec/lib/sinatra/validation.rb
    incubator/deltacloud/trunk/server/libexec/views/docs/
    incubator/deltacloud/trunk/server/libexec/views/docs/collection.html.haml
    incubator/deltacloud/trunk/server/libexec/views/docs/index.html.haml
    incubator/deltacloud/trunk/server/libexec/views/docs/operation.html.haml
    incubator/deltacloud/trunk/server/libexec/views/error.html.haml
Modified:
    incubator/deltacloud/trunk/server/libexec/public/stylesheets/compiled/application.css
    incubator/deltacloud/trunk/server/libexec/views/api/show.html.haml
    incubator/deltacloud/trunk/server/libexec/views/api/show.xml.haml

Added: incubator/deltacloud/trunk/server/libexec/lib/sinatra/rabbit.rb
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/lib/sinatra/rabbit.rb?rev=962197&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/lib/sinatra/rabbit.rb (added)
+++ incubator/deltacloud/trunk/server/libexec/lib/sinatra/rabbit.rb Thu Jul  8 23:35:51 2010
@@ -0,0 +1,264 @@
+require 'sinatra/base'
+require 'sinatra/url_for'
+
+module Sinatra
+
+  module Rabbit
+
+    class DuplicateParamException < Exception; end
+    class DuplicateOperationException < Exception; end
+    class DuplicateCollectionException < Exception; end
+    class ValidationFailure < Exception; end
+
+    class Operation
+      attr_reader :name, :method
+
+      STANDARD = {
+        :index => { :method => :get, :member => false },
+        :show =>  { :method => :get, :member => true },
+        :create => { :method => :post, :member => false },
+        :update => { :method => :put, :member => true },
+        :destroy => { :method => :delete, :member => true }
+      }
+
+      def initialize(coll, name, opts, &block)
+        @name = name.to_sym
+        opts = STANDARD[@name].merge(opts) if standard?
+        @collection = coll
+        raise "No method for operation #{name}" unless opts[:method]
+        @method = opts[:method].to_sym
+        @member = opts[:member]
+        @description = ""
+        @params = {}
+        instance_eval(&block) if block_given?
+        generate_documentation
+      end
+
+      def standard?
+        STANDARD.keys.include?(name)
+      end
+
+      def description(text="")
+        return @description if text.blank?
+        @description = text
+      end
+
+      def generate_documentation
+        coll, oper = @collection, self
+        ::Sinatra::Application.get("/api/docs/#{@collection.name}/#{@name}") do
+          @collection, @operation = coll, oper
+          respond_to do |format|
+            format.html { haml :'docs/operation' }
+          end
+        end
+      end
+
+      def param(*args)
+        raise DuplicateParamException if @params[args[0]]
+        spec = {
+          :class => args[1] || :string,
+          :type => args[2] || :optional,
+          :options => args[3] || [],
+          :description => args[4] || '' }
+        @params[args[0]] = spec
+      end
+
+      def params
+        @params
+      end
+
+      def control(&block)
+        @control = Proc.new do
+          validate_parameters(params, @params)
+          instance_eval(&block)
+        end
+      end
+
+      def prefix
+        # FIXME: Make the /api prefix configurable
+        "/api"
+      end
+
+      def path(args = {})
+        l_prefix = args[:prefix] ? args[:prefix] : prefix
+        if @member
+          if standard?
+            "#{l_prefix}/#{@collection.name}/:id"
+          else
+            "#{l_prefix}/#{@collection.name}/:id/#{name}"
+          end
+        else
+          "#{l_prefix}/#{@collection.name}"
+        end
+      end
+
+      def generate
+        ::Sinatra::Application.send(@method, path, {}, &@control)
+        # Set up some Rails-like URL helpers
+        if name == :index
+          gen_route "#{@collection.name}_url"
+        elsif name == :show
+          gen_route "#{@collection.name.to_s.singularize}_url"
+        else
+          gen_route "#{name}_#{@collection.name.to_s.singularize}_url"
+        end
+      end
+
+      private
+      def gen_route(name)
+        route_url = path
+        if @member
+          ::Sinatra::Application.send(:define_method, name) do |id, *args|
+            url = query_url(route_url, args[0])
+            url_for url.gsub(/:id/, id.to_s), :full
+          end
+        else
+          ::Sinatra::Application.send(:define_method, name) do |*args|
+            url = query_url(route_url, args[0])
+            url_for url, :full
+          end
+        end
+      end
+    end
+
+    class Collection
+      attr_reader :name, :operations
+
+      def initialize(name, &block)
+        @name = name
+        @description = ""
+        @operations = {}
+        instance_eval(&block) if block_given?
+        generate_documentation
+      end
+
+      # Set/Return description for collection
+      # If first parameter is not present, full description will be
+      # returned.
+      def description(text='')
+        return @description if text.blank?
+        @description = text
+      end
+
+      def generate_documentation
+        coll, oper = self, @operations
+        ::Sinatra::Application.get("/api/docs/#{@name}") do
+          @collection, @operations = coll, oper
+          respond_to do |format|
+            format.html { haml :'docs/collection' }
+          end
+        end
+      end
+
+      # Add a new operation for this collection. For the standard REST
+      # operations :index, :show, :update, and :destroy, we already know
+      # what method to use and whether this is an operation on the URL for
+      # individual elements or for the whole collection.
+      #
+      # For non-standard operations, options must be passed:
+      #  :method : one of the HTTP methods
+      #  :member : whether this is an operation on the collection or an
+      #            individual element (FIXME: custom operations on the
+      #            collection will use a nonsensical URL) The URL for the
+      #            operation is the element URL with the name of the operation
+      #            appended
+      #
+      # This also defines a helper method like show_flavor_url that returns
+      # the URL to this operation (in request context)
+      def operation(name, opts = {}, &block)
+        raise DuplicateOperationException if @operations[name]
+        @operations[name] = Operation.new(self, name, opts, &block)
+      end
+
+      def generate
+        operations.values.each { |op| op.generate }
+        app = ::Sinatra::Application
+        collname = name # Work around Ruby's weird scoping/capture
+        app.send(:define_method, "#{name.to_s.singularize}_url") do |id|
+            url_for "/api/#{collname}/#{id}", :full
+        end
+
+        if index_op = operations[:index]
+          app.send(:define_method, "#{name}_url") do
+            url_for index_op.path.gsub(/\/\?$/,''), :full
+          end
+        end
+      end
+    end
+
+    def collections
+      @collections ||= {}
+    end
+
+    # Create a new collection. NAME should be the pluralized name of the
+    # collection.
+    #
+    # Adds a helper method #{name}_url which returns the URL to the :index
+    # operation on this collection.
+    def collection(name, &block)
+      raise DuplicateCollectionException if collections[name]
+      collections[name] = Collection.new(name, &block)
+      collections[name].generate
+    end
+
+    # Generate a root route for API docs
+    get '/api/docs\/?' do
+      respond_to do |format|
+        format.html { haml :'docs/index' }
+      end
+    end
+
+  end
+
+  module RabbitHelper
+    def query_url(url, params)
+      return url if params.nil? || params.empty?
+      url + "?#{URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))}"
+    end
+
+    def entry_points
+      collections.values.inject([]) do |m, coll|
+        url = url_for coll.operations[:index].path, :full
+        m << [ coll.name, url ]
+      end
+    end
+  end
+
+  register Rabbit
+  helpers RabbitHelper
+end
+
+class String
+  # Rails defines this for a number of other classes, including Object
+  # see activesupport/lib/active_support/core_ext/object/blank.rb
+  def blank?
+      self !~ /\S/
+  end
+
+  # Title case.
+  #
+  #   "this is a string".titlecase
+  #   => "This Is A String"
+  #
+  # CREDIT: Eliazar Parra
+  # Copied from facets
+  def titlecase
+    gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
+  end
+
+  def pluralize
+    self + "s"
+  end
+
+  def singularize
+    self.gsub(/s$/, '')
+  end
+
+  def underscore
+      gsub(/::/, '/').
+          gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
+          gsub(/([a-z\d])([A-Z])/,'\1_\2').
+          tr("-", "_").
+          downcase
+  end
+end

Added: incubator/deltacloud/trunk/server/libexec/lib/sinatra/validation.rb
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/lib/sinatra/validation.rb?rev=962197&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/lib/sinatra/validation.rb (added)
+++ incubator/deltacloud/trunk/server/libexec/lib/sinatra/validation.rb Thu Jul  8 23:35:51 2010
@@ -0,0 +1,28 @@
+class ValidationFailure < Exception
+  attr_reader :name, :spec, :msg
+  def initialize(name, spec, msg='')
+    @name, @spec, @msg = name, spec, msg
+  end
+end
+
+error ValidationFailure do
+  content_type 'text/xml', :charset => 'utf-8'
+  @error = request.env['sinatra.error']
+  haml :error, :layout => false
+end
+
+def validate_parameters(values, parameters)
+  require 'pp'
+  parameters.each_key do |p|
+    if parameters[p][:type].eql?(:required) and not values[p.to_s]
+      raise ValidationFailure.new(p, parameters[p], 'Required parameter not found')
+    end
+    if parameters[p][:type].eql?(:required) and not parameters[p][:options].empty? and not parameters[p][:options].include?(values[p.to_s])
+      raise ValidationFailure.new(p, parameters[p], 'Wrong value for required parameter')
+    end
+    if parameters[p][:type].eql?(:optional) and not parameters[p][:options].empty? and
+      not values[p.to_s].nil? and not parameters[p][:options].include?(values[p.to_s])
+      raise ValidationFailure.new(p, parameters[p], 'Wrong value for optional parameter')
+    end
+  end
+end

Modified: incubator/deltacloud/trunk/server/libexec/public/stylesheets/compiled/application.css
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/public/stylesheets/compiled/application.css?rev=962197&r1=962196&r2=962197&view=diff
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/public/stylesheets/compiled/application.css (original)
+++ incubator/deltacloud/trunk/server/libexec/public/stylesheets/compiled/application.css Thu Jul  8 23:35:51 2010
@@ -607,3 +607,7 @@ input[type='radio'] {
   display: block;
   margin-bottom: 2em;
   text-align: center; }
+
+table.docs { border : 1px solid #ccc }
+table.docs td { border : 1px solid #ccc }
+table.docs table td { border : none }

Modified: incubator/deltacloud/trunk/server/libexec/views/api/show.html.haml
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/views/api/show.html.haml?rev=962197&r1=962196&r2=962197&view=diff
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/views/api/show.html.haml (original)
+++ incubator/deltacloud/trunk/server/libexec/views/api/show.html.haml Thu Jul  8 23:35:51 2010
@@ -2,6 +2,14 @@
   API v#{@version}
 
 %ul
-  - for entry_point in @entry_points
-    %li 
-      = link_to entry_point[0].to_s.titlecase, entry_point[1]
+  - collections.keys.sort_by { |k| k.to_s }.each do |key|
+    %li
+      = link_to key.to_s.gsub('_', ' ').titlecase, url_for("/api/#{key}")
+      %dl
+        - collections[key].operations.each_key do |op|
+          - next if [:index, :show].include?(op)
+          %dt
+            = op
+  %li
+    %strong
+      %a{:href => "/api/docs"} Documentation (v#{@version})

Modified: incubator/deltacloud/trunk/server/libexec/views/api/show.xml.haml
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/views/api/show.xml.haml?rev=962197&r1=962196&r2=962197&view=diff
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/views/api/show.xml.haml (original)
+++ incubator/deltacloud/trunk/server/libexec/views/api/show.xml.haml Thu Jul  8 23:35:51 2010
@@ -1,3 +1,3 @@
 %api{ :version=>@version, :driver=>DRIVER }
-  - for entry_point in @entry_points
+  - for entry_point in entry_points
     %link{ :rel=>entry_point[0], :href=>entry_point[1] }

Added: incubator/deltacloud/trunk/server/libexec/views/docs/collection.html.haml
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/views/docs/collection.html.haml?rev=962197&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/views/docs/collection.html.haml (added)
+++ incubator/deltacloud/trunk/server/libexec/views/docs/collection.html.haml Thu Jul  8 23:35:51 2010
@@ -0,0 +1,20 @@
+%h2
+  = @collection.name.to_s.titlecase
+
+%p #{@collection.description}
+
+%br
+
+%h3 Operations:
+
+%table
+  %thead
+    %tr
+      %th Name
+      %th Description
+  %tbody
+    - @operations.keys.sort_by { |k| k.to_s }.each do |operation|
+      %tr
+        %td{:style => "width:15em"}
+          %a{:href => "/api/docs/#{@collection.name.to_s}/#{operation}"} #{operation}
+        %td{:style => "width:10em"} #{@operations[operation].description}

Added: incubator/deltacloud/trunk/server/libexec/views/docs/index.html.haml
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/views/docs/index.html.haml?rev=962197&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/views/docs/index.html.haml (added)
+++ incubator/deltacloud/trunk/server/libexec/views/docs/index.html.haml Thu Jul  8 23:35:51 2010
@@ -0,0 +1,15 @@
+%h2 API Documentation
+
+%h3 Collections:
+
+%table
+  %thead
+    %tr
+      %th Name
+      %th Description
+  %tbody
+    - collections.keys.sort_by { |k| k.to_s }.each do |collection|
+      %tr
+        %td{:style => "width:15em"}
+          %a{:href => "/api/docs/#{collection}"} #{collection}
+        %td{:style => "width:10em"} #{collections[collection].description}

Added: incubator/deltacloud/trunk/server/libexec/views/docs/operation.html.haml
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/views/docs/operation.html.haml?rev=962197&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/views/docs/operation.html.haml (added)
+++ incubator/deltacloud/trunk/server/libexec/views/docs/operation.html.haml Thu Jul  8 23:35:51 2010
@@ -0,0 +1,31 @@
+%h2
+  %a{:href => "/api/docs/#{@collection.name.to_s}"} #{@collection.name.to_s.titlecase}
+  #{'::'}
+  #{@operation.name}
+
+%p #{@operation.description}
+
+%br
+%h3
+  URL:
+  %u
+    = "/api/#{@collection.name.to_s}/#{@operation.name.to_s}"
+%br
+%h3 Parameters:
+
+
+%table
+  %thead
+    %tr
+      %th Name
+      %th Type
+      %th Class
+      %th Valid values
+  %tbody
+    - @operation.params.each_key do |p|
+      %tr
+        %td{:style => "width:15em"}
+          %em #{p}
+        %td{:style => "width:10em"} #{@operation.params[p][:type]}
+        %td #{@operation.params[p][:class]}
+        %td{:style => "width:10em"} #{@operation.params[p][:options].join(',')}

Added: incubator/deltacloud/trunk/server/libexec/views/error.html.haml
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/views/error.html.haml?rev=962197&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/views/error.html.haml (added)
+++ incubator/deltacloud/trunk/server/libexec/views/error.html.haml Thu Jul  8 23:35:51 2010
@@ -0,0 +1,7 @@
+%error{:url => "#{request.env['REQUEST_URI']}"}
+  %parameter #{@error.name}
+  %msg #{@error.msg}
+  - unless @error.spec[:options].empty?
+    %valid_options
+      - @error.spec[:options].each do |v|
+        %value #{v}