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}