On Jun 28, 2011, at 6:46 PM, Francesco Vollero wrote: > Ack, but i had few problems with whitespaces in the code. Fix it please.
Thanks, I tried to remove them. Pushed. Will ping issue creators to test it. -- Michal > > > On Tue, Jun 28, 2011 at 03:51:42PM +0200, [email protected] wrote: >> From: Michal Fojtik <[email protected]> >> >> >> Signed-off-by: Michal fojtik <[email protected]> >> --- >> client/specs/content_spec.rb | 152 ++++++++++++++ >> server/lib/deltacloud/base_driver/exceptions.rb | 7 + >> server/lib/sinatra/rack_accept.rb | 152 ++++++++++++++ >> server/lib/sinatra/respond_to.rb | 248 >> ----------------------- >> server/server.rb | 7 +- >> 5 files changed, 315 insertions(+), 251 deletions(-) >> create mode 100644 client/specs/content_spec.rb >> create mode 100644 server/lib/sinatra/rack_accept.rb >> delete mode 100644 server/lib/sinatra/respond_to.rb >> >> diff --git a/client/specs/content_spec.rb b/client/specs/content_spec.rb >> new file mode 100644 >> index 0000000..78ae937 >> --- /dev/null >> +++ b/client/specs/content_spec.rb >> @@ -0,0 +1,152 @@ >> +# >> +# Copyright (C) 2009-2011 Red Hat, Inc. >> +# >> +# Licensed to the Apache Software Foundation (ASF) under one or more >> +# contributor license agreements. See the NOTICE file distributed with >> +# this work for additional information regarding copyright ownership. The >> +# ASF licenses this file to you under the Apache License, Version 2.0 (the >> +# "License"); you may not use this file except in compliance with the >> +# License. You may obtain a copy of the License at >> +# >> +# http://www.apache.org/licenses/LICENSE-2.0 >> +# >> +# Unless required by applicable law or agreed to in writing, software >> +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT >> +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the >> +# License for the specific language governing permissions and limitations >> +# under the License. >> + >> +require 'specs/spec_helper' >> + >> +def client >> + RestClient::Resource.new(API_URL) >> +end >> + >> +def headers(header) >> + encoded_credentials = >> ["#{API_NAME}:#{API_PASSWORD}"].pack("m0").gsub(/\n/,'') >> + { :authorization => "Basic " + encoded_credentials }.merge(header) >> +end >> + >> +describe "return JSON" do >> + >> + it 'should return JSON when using application/json, */*' do >> + header_hash = { >> + 'Accept' => "application/json, */*" >> + } >> + client.get(header_hash) do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^application\/json/ >> + end >> + end >> + >> + it 'should return JSON when using just application/json' do >> + header_hash = { >> + 'Accept' => "application/json" >> + } >> + client.get(header_hash) do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^application\/json/ >> + end >> + end >> + >> +end >> + >> +describe "return HTML in different browsers" do >> + >> + it "wants XML using format parameter" do >> + client['?format=xml'].get('Accept' => 'application/xhtml+xml') do >> |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^application\/xml/ >> + end >> + end >> + >> + it "wants HTML using format parameter and accept set to XML" do >> + client['?format=html'].get('Accept' => 'application/xml') do |response, >> request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^text\/html/ >> + end >> + end >> + >> + it "wants a PNG image" do >> + client['instance_states?format=png'].get('Accept' => 'image/png') do >> |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^image\/png/ >> + end >> + end >> + >> + it "doesn't have accept header" do >> + client.get('Accept' => '') do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^application\/xml/ >> + end >> + end >> + >> + it "can handle unknown formats" do >> + client.get('Accept' => 'format/unknown') do |response, request, &block| >> + response.code.should == 406 >> + end >> + end >> + >> + it "wants explicitly XML" do >> + client.get('Accept' => 'application/xml') do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^application\/xml/ >> + end >> + end >> + >> + it "Internet Explorer" do >> + header_hash = { >> + 'Accept' => "text/html, application/xhtml+xml, */*", >> + 'User-agent' => "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; >> en-US)" >> + } >> + client.get(header_hash) do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^text\/html/ >> + end >> + end >> + >> + it "Mozilla Firefox" do >> + client.get('Accept' => >> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") do >> |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^text\/html/ >> + end >> + end >> + >> + it "Chrome" do >> + header_hash = { >> + 'Accept' => >> "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", >> + 'User-agent' => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) >> AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1" >> + } >> + client.get(header_hash) do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^text\/html/ >> + end >> + end >> + >> + it "Safari" do >> + header_hash = { >> + 'Accept' => >> "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", >> + 'User-agent' => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; >> da-dk) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 >> Safari/533.21.1" >> + } >> + client.get(header_hash) do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^text\/html/ >> + end >> + end >> + >> + it "Opera" do >> + header_hash = { >> + 'Accept' => "text/html, application/xml;q=0.9, application/xhtml+xml, >> image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1", >> + 'User-agent' => "Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 >> Version/11.11" >> + } >> + client.get(header_hash) do |response, request, &block| >> + response.code.should == 200 >> + response.headers[:content_type].should =~ /^text\/html/ >> + end >> + end >> + >> + >> + >> + >> + >> +end >> diff --git a/server/lib/deltacloud/base_driver/exceptions.rb >> b/server/lib/deltacloud/base_driver/exceptions.rb >> index 391910f..d95191c 100644 >> --- a/server/lib/deltacloud/base_driver/exceptions.rb >> +++ b/server/lib/deltacloud/base_driver/exceptions.rb >> @@ -21,6 +21,13 @@ module Deltacloud >> end >> end >> >> + class UnknownMediaTypeError < DeltacloudException >> + def initialize(e, message=nil) >> + message ||= e.message >> + super(406, e.class.name, message, e.backtrace) >> + end >> + end >> + >> class ValidationFailure < DeltacloudException >> def initialize(e, message=nil) >> message ||= e.message >> diff --git a/server/lib/sinatra/rack_accept.rb >> b/server/lib/sinatra/rack_accept.rb >> new file mode 100644 >> index 0000000..9dddadd >> --- /dev/null >> +++ b/server/lib/sinatra/rack_accept.rb >> @@ -0,0 +1,152 @@ >> +# respond_to (The MIT License) >> + >> +# Permission is hereby granted, free of charge, to any person obtaining a >> copy of this software >> +# and associated documentation files (the 'Software'), to deal in the >> Software without restriction, >> +# including without limitation the rights to use, copy, modify, merge, >> publish, distribute, >> +# sublicense, and/or sell copies of the Software, and to permit persons to >> whom the Software is >> +# furnished to do so, subject to the following conditions: >> +# >> +# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS >> OR IMPLIED, INCLUDING BUT >> +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A >> PARTICULAR PURPOSE AND >> +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE >> LIABLE FOR ANY CLAIM, >> +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR >> OTHERWISE, ARISING FROM, >> +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN >> THE SOFTWARE >> + >> +require 'sinatra/base' >> +require 'rack/accept' >> + >> +use Rack::Accept >> + >> +module Rack >> + >> + module RespondTo >> + >> + # This method is triggered after this helper is registred >> + # within Sinatra. >> + # We need to overide the default render method to supply correct path >> to the >> + # template, since Sinatra is by default looking in the current __FILE__ >> path >> + def self.registered(app) >> + app.helpers Rack::RespondTo::Helpers >> + app.class_eval do >> + alias :render_without_format :render >> + def render(*args, &block) >> + begin >> + assumed_layout = args[1] == :layout >> + args[1] = "#{args[1]}.#{format}".to_sym if >> args[1].is_a?(::Symbol) >> + render_without_format *args, &block >> + rescue Errno::ENOENT => e >> + raise "ERROR: Missing template: #{args[1]}.#{args[0]}" unless >> assumed_layout >> + raise e >> + end >> + end >> + private :render >> + end >> + end >> + >> + module Helpers >> + >> + # This code was inherited from respond_to plugin >> + # http://github.com/cehoffman/sinatra-respond_to >> + # >> + # This method is used to overide the default content_type returned >> from >> + # rack-accept middleware. >> + def self.included(klass) >> + klass.class_eval do >> + alias :content_type_without_save :content_type >> + def content_type(*args) >> + content_type_without_save *args >> + request.env['rack-accept.format'] = args.first.to_sym >> + response['Content-Type'] >> + end >> + end >> + end >> + >> + def format(val=nil) >> + request.env['rack-accept.format'] ||= val >> + request.env['rack-accept.format'].to_sym >> + end >> + >> + def static_file?(path) >> + public_dir = File.expand_path(options.public) >> + path = File.expand_path(File.join(public_dir, unescape(path))) >> + path[0, public_dir.length] == public_dir && File.file?(path) >> + end >> + >> + def respond_to(&block) >> + wants = {} >> + def wants.method_missing(type, *args, &handler) >> + self[type] = handler >> + end >> + yield wants >> + raise >> Deltacloud::ExceptionHandler::UnknownMediaTypeError::new(nil, "Unknown >> format") unless wants[format] >> + wants[format].call >> + end >> + >> + end >> + >> + end >> + >> + class MediaType < Sinatra::Base >> + >> + include Rack::RespondTo::Helpers >> + >> + # Define supported media types here >> + # The :return key stands for content-type which will be returned >> + # The :match key stands for the matching Accept header >> + ACCEPTED_MEDIA_TYPES = { >> + :xml => { :return => 'application/xml', :match => ['application/xml', >> 'text/xml'] }, >> + :json => { :return => 'application/json', :match => >> ['application/json'] }, >> + :html => { :return => 'text/html', :match => >> ['application/xhtml+xml', 'text/html'] }, >> + :png => { :return => 'image/png', :match => ['image/png'] }, >> + :gv => { :return => 'application/ghostscript', :match => >> ['application/ghostscript'] } >> + } >> + >> + def call(env) >> + accept, index = env['rack-accept.request'], {} >> + >> + # Skip everything when 'format' parameter is set in URL >> + if env['rack.request.query_hash']["format"] >> + media_type = case env['rack.request.query_hash']["format"] >> + when 'html' then :html >> + when 'xml' then :xml >> + when 'json' then :json >> + when 'gv' then :gv >> + when 'png' then :png >> + end >> + index[media_type] = 1 if media_type >> + else >> + # Sort all requested media types in Accept using their 'q' values >> + sorted_media_types = accept.media_type.qvalues.to_a.sort{ |a,b| >> b[1]<=>a[1] }.collect { |t| t.first } >> + # If Accept header is missing or is empty, fallback to XML format >> + sorted_media_types << 'application/xml' if sorted_media_types.empty? >> + # Choose the right format with the media type according to the >> priority >> + ACCEPTED_MEDIA_TYPES.each do |format, definition| >> + definition[:match].each do |media_type| >> + break if index[format] = sorted_media_types.index(media_type) >> + end >> + end >> + # Reject formats with no/nil priority >> + index.reject! { |format, priority| not priority } >> + end >> + >> + # If after all we don't have any matching format assume that client >> has >> + # requested unknown/wrong media type and throw an 406 error with no >> body >> + if index.keys.empty? >> + status, headers, response = 406, {}, "" >> + else >> + media_type = index.to_a.sort{ |a, b| a[1]<=>b[1] }.first[0] >> + # Set this environment variable for futher pickup by the 'format' >> helper >> + # on top >> + env['rack-accept.format'] = media_type >> + status, headers, response = @app.call(env) >> + # Overide the Content-type with :return value of matching format >> + headers['Content-Type'] = ACCEPTED_MEDIA_TYPES[media_type][:return] >> + end >> + >> + [status, headers, response] >> + end >> + >> + end >> + >> +end >> + >> diff --git a/server/lib/sinatra/respond_to.rb >> b/server/lib/sinatra/respond_to.rb >> deleted file mode 100644 >> index 139573b..0000000 >> --- a/server/lib/sinatra/respond_to.rb >> +++ /dev/null >> @@ -1,248 +0,0 @@ >> -# respond_to (The MIT License) >> - >> -# Permission is hereby granted, free of charge, to any person obtaining a >> copy of this software >> -# and associated documentation files (the 'Software'), to deal in the >> Software without restriction, >> -# including without limitation the rights to use, copy, modify, merge, >> publish, distribute, >> -# sublicense, and/or sell copies of the Software, and to permit persons to >> whom the Software is >> -# furnished to do so, subject to the following conditions: >> -# >> -# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS >> OR IMPLIED, INCLUDING BUT >> -# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A >> PARTICULAR PURPOSE AND >> -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE >> LIABLE FOR ANY CLAIM, >> -# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR >> OTHERWISE, ARISING FROM, >> -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN >> THE SOFTWARE >> - >> -require 'sinatra/base' >> -require 'rack/accept' >> - >> -use Rack::Accept >> - >> -module Sinatra >> - module RespondTo >> - >> - class MissingTemplate < Sinatra::NotFound; end >> - >> - # Define all MIME types you want to support here. >> - # This conversion table will be used for auto-negotiation >> - # with browser in sinatra when no 'format' parameter is specified. >> - >> - SUPPORTED_ACCEPT_HEADERS = { >> - :xml => [ >> - 'text/xml', >> - 'application/xml' >> - ], >> - :html => [ >> - 'text/html', >> - 'application/xhtml+xml' >> - ], >> - :json => [ >> - 'application/json' >> - ] >> - } >> - >> - # We need to pass array of available response types to >> - # best_media_type method >> - def accept_to_array >> - SUPPORTED_ACCEPT_HEADERS.keys.collect do |key| >> - SUPPORTED_ACCEPT_HEADERS[key] >> - end.flatten >> - end >> - >> - # Then, when we get best media type for response, we need >> - # to know which format to choose >> - def lookup_format_from_mime(mime) >> - SUPPORTED_ACCEPT_HEADERS.keys.each do |format| >> - return format if SUPPORTED_ACCEPT_HEADERS[format].include?(mime) >> - end >> - end >> - >> - def self.registered(app) >> - >> - app.helpers RespondTo::Helpers >> - >> - app.before do >> - >> - # Skip development error image and static content >> - next if self.class.development? && request.path_info =~ >> %r{/__sinatra__/.*?.png} >> - next if options.static? && options.public? && (request.get? || >> request.head?) && static_file?(request.path_info) >> - >> - # Remove extension from URI >> - # Extension will be available as a 'extension' method >> (extension=='txt') >> - >> - extension request.path_info.match(/\.([^\.\/]+)$/).to_a.first >> - >> - # If ?format= is present, ignore all Accept negotiations because >> - # we are not dealing with browser >> - if request.params.has_key? 'format' >> - format params['format'].to_sym >> - end >> - >> - # Let's make a little exception here to handle >> - # /api/instance_states[.gv/.png] calls >> - if extension.eql?('gv') >> - format :gv >> - elsif extension.eql?('png') >> - format :png >> - end >> - >> - # Get Rack::Accept::Response object and find best possible >> - # mime type to output. >> - # This negotiation works fine with latest rest-client gem: >> - # >> - # RestClient.get 'http://localhost:3001/api', {:accept => :json } => >> - # 'application/json' >> - # RestClient.get 'http://localhost:3001/api', {:accept => :xml } => >> - # 'application/xml' >> - # >> - # Also browsers like Firefox (3.6.x) and Chromium reporting >> - # 'application/xml+xhtml' which is recognized as :html reponse >> - # In browser you can force output using ?format=[format] parameter. >> - >> - rack_accept = env['rack-accept.request'] >> - >> - if rack_accept.media_type.to_s.strip.eql?('Accept:') >> - format :xml >> - elsif is_chrome? >> - format :html >> - else >> - format >> lookup_format_from_mime(rack_accept.best_media_type(accept_to_array)) >> - end >> - >> - end >> - >> - app.class_eval do >> - >> - # Simple helper to detect Chrome based browsers >> - # which have screwed up they Accept headers. >> - # Set HTML as default output format here >> - def is_chrome? >> - true if env['HTTP_USER_AGENT'] =~ /Chrome/ >> - end >> - >> - # This code was copied from respond_to plugin >> - # http://github.com/cehoffman/sinatra-respond_to >> - # MIT License >> - alias :render_without_format :render >> - def render(*args, &block) >> - assumed_layout = args[1] == :layout >> - args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol) >> - render_without_format *args, &block >> - rescue Errno::ENOENT => e >> - raise MissingTemplate, "#{args[1]}.#{args[0]}" unless >> assumed_layout >> - raise e >> - end >> - private :render >> - end >> - >> - # This code was copied from respond_to plugin >> - # http://github.com/cehoffman/sinatra-respond_to >> - app.configure :development do |dev| >> - dev.error MissingTemplate do >> - content_type :html, :charset => 'utf-8' >> - response.status = request.env['sinatra.error'].code >> - >> - engine = request.env['sinatra.error'].message.split('.').last >> - engine = 'haml' unless ['haml', 'builder', 'erb'].include? engine >> - >> - path = File.basename(request.path_info) >> - path = "root" if path.nil? || path.empty? >> - >> - format = engine == 'builder' ? 'xml' : 'html' >> - >> - layout = case engine >> - when 'haml' then "!!!\n%html\n %body= yield" >> - when 'erb' then "<html>\n <body>\n <%= yield %>\n >> </body>\n</html>" >> - end >> - >> - layout = >> "<small>app.#{format}.#{engine}</small>\n<pre>#{escape_html(layout)}</pre>" >> - >> - (<<-HTML).gsub(/^ {10}/, '') >> - <!DOCTYPE html> >> - <html> >> - <head> >> - <style type="text/css"> >> - body { >> text-align:center;font-family:helvetica,arial;font-size:22px; >> - color:#888;margin:20px} >> - #c {margin:0 auto;width:500px;text-align:left;} >> - small {float:right;clear:both;} >> - pre >> {clear:both;text-align:left;font-size:70%;width:500px;margin:0 auto;} >> - </style> >> - </head> >> - <body> >> - <h2>Sinatra can't find >> #{request.env['sinatra.error'].message}</h2> >> - <img src='/__sinatra__/500.png'> >> - <pre>#{request.env['sinatra.error'].backtrace.join("\n")}</pre> >> - <div id="c"> >> - <small>application.rb</small> >> - <pre>#{request.request_method.downcase} >> '#{request.path_info}' do\n respond_to do |wants|\n wants.#{format} { >> #{engine} :#{path} }\n end\nend</pre> >> - </div> >> - </body> >> - </html> >> - HTML >> - end >> - >> - end >> - end >> - >> - module Helpers >> - >> - # This code was copied from respond_to plugin >> - # http://github.com/cehoffman/sinatra-respond_to >> - def self.included(klass) >> - klass.class_eval do >> - alias :content_type_without_save :content_type >> - def content_type(*args) >> - content_type_without_save *args >> - @_format = args.first.to_sym >> - response['Content-Type'] >> - end >> - end >> - end >> - >> - def static_file?(path) >> - public_dir = File.expand_path(options.public) >> - path = File.expand_path(File.join(public_dir, unescape(path))) >> - >> - path[0, public_dir.length] == public_dir && File.file?(path) >> - end >> - >> - >> - # Extension holds trimmed extension. This is extra usefull >> - # when you want to build original URI (with extension) >> - # You can simply call "#{request.env['REQUEST_URI']}.#{extension}" >> - def extension(val=nil) >> - @_extension ||= val >> - @_extension >> - end >> - >> - # This helper will holds current format. Helper should be >> - # accesible from all places in Sinatra >> - def format(val=nil) >> - @_format ||= val >> - @_format >> - end >> - >> - def respond_to(&block) >> - wants = {} >> - >> - def wants.method_missing(type, *args, &handler) >> - self[type] = handler >> - end >> - >> - # Set proper content-type and encoding for >> - # text based formats >> - if [:xml, :gv, :html, :json].include?(format) >> - content_type format, :charset => 'utf-8' >> - end >> - yield wants >> - # Raise this error if requested format is not defined >> - # in respond_to { } block. >> - raise MissingTemplate if wants[format].nil? >> - >> - wants[format].call >> - end >> - >> - end >> - >> - end >> -end >> diff --git a/server/server.rb b/server/server.rb >> index 104ea9c..787d8e4 100644 >> --- a/server/server.rb >> +++ b/server/server.rb >> @@ -17,7 +17,7 @@ require 'sinatra' >> require 'deltacloud' >> require 'drivers' >> require 'json' >> -require 'sinatra/respond_to' >> +require 'sinatra/rack_accept' >> require 'sinatra/static_assets' >> require 'sinatra/rabbit' >> require 'sinatra/lazy_auth' >> @@ -35,10 +35,13 @@ set :version, '0.3.0' >> include Deltacloud::Drivers >> set :drivers, Proc.new { driver_config } >> >> +Sinatra::Application.register Rack::RespondTo >> + >> use Rack::ETag >> use Rack::Runtime >> use Rack::MatrixParams >> use Rack::DriverSelect >> +use Rack::MediaType >> >> configure do >> set :raise_errors => false >> @@ -63,8 +66,6 @@ error do >> report_error >> end >> >> -Sinatra::Application.register Sinatra::RespondTo >> - >> # Redirect to /api >> get '/' do redirect root_url, 301; end >> >> -- >> 1.7.4.1 >> ------------------------------------------------------ Michal Fojtik, [email protected] Deltacloud API: http://deltacloud.org
