incubator-deltacloud-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mfoj...@redhat.com
Subject [PATCH core 3/4] Rewritten respond_to plugin to be more helpfull. Extensions are now ignored and there is a new helper for handling it. Browser content-negotiation is now done using rack-accept gem.
Date Mon, 27 Sep 2010 15:22:54 GMT
From: Michal Fojtik <mfojtik@redhat.com>

---
 server/lib/sinatra/respond_to.rb |  313 ++++++++++++++++++--------------------
 server/server.rb                 |    8 +-
 2 files changed, 157 insertions(+), 164 deletions(-)

diff --git a/server/lib/sinatra/respond_to.rb b/server/lib/sinatra/respond_to.rb
index 926b10d..b4efac4 100644
--- a/server/lib/sinatra/respond_to.rb
+++ b/server/lib/sinatra/respond_to.rb
@@ -1,73 +1,133 @@
+# 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 'sinatra/accept_media_types'
+require 'rack/accept'
 
-# Accept header parsing was looked at but deemed
-# too much of an irregularity to deal with.  Problems with the header
-# differences from IE, Firefox, Safari, and every other UA causes
-# problems with the expected output.  The general expected behavior
-# would be serve html when no extension provided, but most UAs say
-# they will accept application/xml with out a quality indicator, meaning
-# you'd get the xml block served insead.  Just plain retarded, use the
-# extension and you'll never be suprised.
+use Rack::Accept
 
 module Sinatra
   module RespondTo
-    class UnhandledFormat < Sinatra::NotFound; end
-    class MissingTemplate < Sinatra::NotFound
-      def code; 500 end
+
+    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
 
-    TEXT_MIME_TYPES = [:txt, :html, :js, :json, :xml, :rss, :atom, :css, :asm, :c, :cc, :conf,
-                       :csv, :cxx, :diff, :dtd, :f, :f77, :f90, :for, :gemspec, :h, :hh,
:htm,
-                       :log, :mathml, :mml, :p, :pas, :pl, :pm, :py, :rake, :rb, :rdf, :rtf,
:ru,
-                       :s, :sgm, :sgml, :sh, :svg, :svgz, :text, :wsdl, :xhtml, :xsl, :xslt,
:yaml,
-                       :yml, :ics, :png]
+    # 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.set :default_charset, 'utf-8'
-      app.set :default_content, :html
-      app.set :assume_xhr_is_js, true
+      app.helpers RespondTo::Helpers
 
-    #deltacloud: removed the code that tried to 'guess' content based on extension
-    #as this broke blobstore api (stripping blob names). Use ?format if present, otherwise

-    #use http accept, otherwise set to default
       app.before do
-        if (params[:format])
-          @mime_types = [Helpers::mime_type(params[:format])]
-          format params[:format]
-        elsif env['HTTP_ACCEPT'].nil? || env['HTTP_ACCEPT'].empty?
-          ext = options.default_content
+
+        # 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')
+       
+        request.path_info.sub! %r{\.([^\./]+)$}, ''
+        extension $1
+
+        # 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
-      end
 
-     app.configure :development do |dev|
-        dev.error UnhandledFormat do
-          content_type :html, :charset => 'utf-8'
+        # 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
+        else
+          format lookup_format_from_mime(rack_accept.best_media_type(accept_to_array))
+        end
 
-          (<<-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}
-            </style>
-          </head>
-          <body>
-            <h2>Sinatra doesn't know this ditty.</h2>
-            <img src='/__sinatra__/404.png'>
-            <div id="c">
-              Try this:
-              <pre>#{request.request_method.downcase} '#{request.path_info}' do\n 
respond_to do |wants|\n    wants.#{format} { "Hello World" }\n  end\nend</pre>
-            </div>
-          </body>
-          </html>
-          HTML
+      end
+
+      app.class_eval do
+        # 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
@@ -83,7 +143,6 @@ module Sinatra
           layout = case engine
                    when 'haml' then "!!!\n%html\n  %body= yield"
                    when 'erb' then "<html>\n  <body>\n    <%= yield %>\n
 </body>\n</html>"
-                   when 'builder' then ::Sinatra::VERSION =~ /^1.0/ ? "xml << yield"
: "builder do |xml|\n  xml << yield\nend"
                    end
 
           layout = "<small>app.#{format}.#{engine}</small>\n<pre>#{escape_html(layout)}</pre>"
@@ -97,19 +156,16 @@ module Sinatra
               color:#888;margin:20px}
             #c {margin:0 auto;width:500px;text-align:left;}
             small {float:right;clear:both;}
-            pre {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">
-              Try this:<br />
-              #{layout}
-              <small>#{path}.#{format}.#{engine}</small>
-              <pre>Hello World!</pre>
               <small>application.rb</small>
-              <pre>#{request.request_method.downcase} '#{request.path_info}' do\n 
respond_to do |wants|\n    wants.#{engine == 'builder' ? 'xml' : 'html'} { #{engine} :#{path}#{",\n#{'
'*32}layout => :app" if layout} }\n  end\nend</pre>
+              <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>
@@ -117,75 +173,23 @@ module Sinatra
         end
 
       end
-
-      app.class_eval do
-        private
-          def accept_list
-            @mime_types || Rack::AcceptMediaTypes.new(env['HTTP_ACCEPT'] || '')
-          end
-
-          # Changes in 1.0 Sinatra reuse render for layout so we store
-          # the original value to tell us if this is an automatic attempt
-          # to do a layout call.  If it is, it might fail with Errno::ENOENT
-          # and we want to pass that back to sinatra since it isn't a MissingTemplate
-          # error
-          def render_with_format(*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
-          alias_method :render_without_format, :render
-          alias_method :render, :render_with_format
-
-          if ::Sinatra::VERSION =~ /^0\.9/
-            def lookup_layout_with_format(*args)
-              args[1] = "#{args[1]}.#{format}".to_sym if args[1].is_a?(::Symbol)
-              lookup_layout_without_format *args
-            end
-            alias_method :lookup_layout_without_format, :lookup_layout
-            alias_method :lookup_layout, :lookup_layout_with_format
-          end
-      end
     end
 
     module Helpers
-      # Patch the content_type function to remember the set type
-      # This helps cut down on time in the format helper so it
-      # doesn't have to do a reverse lookup on the header
+
+      # This code was copied from respond_to plugin
+      # http://github.com/cehoffman/sinatra-respond_to
       def self.included(klass)
         klass.class_eval do
-          def content_type_with_save(*args)
+          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
-          alias_method :content_type_without_save, :content_type
-          alias_method :content_type, :content_type_with_save
-        end if ::Sinatra::VERSION =~ /^1.0/
-      end
-
-      def self.mime_type(sym)
-        ::Sinatra::Base.respond_to?(:mime_type) && ::Sinatra::Base.mime_type(sym)
|| ::Sinatra::Base.media_type(sym)
-      end
-
-      def format(val=nil)
-        unless val.nil?
-          mime_type = ::Sinatra::RespondTo::Helpers.mime_type(val)
-          fail "Unknown media type #{val}\nTry registering the extension with a mime type"
if mime_type.nil?
-
-          @_format = val.to_sym
-          response['Content-Type'].sub!(/^[^;]+/, mime_type)
-          charset options.default_charset if Sinatra::RespondTo::TEXT_MIME_TYPES.include?(format)
and format!=:png
         end
-
-        @_format
       end
 
-      # This is mostly just a helper so request.path_info isn't changed when
-      # serving files from the public directory
       def static_file?(path)
         public_dir = File.expand_path(options.public)
         path = File.expand_path(File.join(public_dir, unescape(path)))
@@ -193,60 +197,43 @@ module Sinatra
         path[0, public_dir.length] == public_dir && File.file?(path)
       end
 
-      def charset(val=nil)
-        fail "Content-Type must be set in order to specify a charset" if response['Content-Type'].nil?
 
-        if response['Content-Type'] =~ /charset=[^;]+/
-          response['Content-Type'].sub!(/charset=[^;]+/, (val == '' && '') || "charset=#{val}")
-        else
-          response['Content-Type'] += ";charset=#{val}"
-        end unless val.nil?
-
-        response['Content-Type'][/charset=([^;]+)/, 1]
+      # 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
-
-      def respond_to(&block)
-        wants = Format.new
-        yield wants
-        fmt, type, handler = match_accept_type(accept_list, wants)
-        raise UnhandledFormat if fmt.nil?
-        format fmt
-        handler.nil? ? nil : handler.call
+      
+      # This helper will holds current format. Helper should be
+      # accesible from all places in Sinatra
+      def format(val=nil)
+        @_format ||= val
+        @_format
       end
 
-      def match_accept_type(mime_types, format)
-        selected = []
-        accepted_types = mime_types.map {|type| Regexp.escape(type).gsub(/\\\*/,'.*') }
-        # Fix for Chrome based browsers which returns XML when 'xhtml' is requested.
-        if env['HTTP_USER_AGENT'] =~ /Chrome/ and accepted_types.size>1
-          accepted_types[0], accepted_types[1] = accepted_types[1], accepted_types[0]
-          if accepted_types[0].eql?('application/xhtml\\+xml')
-            accepted_types[0] = 'text/html'
-          end
+      def respond_to(&block)
+        wants = {}
+        
+        def wants.method_missing(type, *args, &handler)
+          self[type] = handler
         end
-        accepted_types.each do |at|
-          format.each do |fmt, ht, handler|
-            (selected = [fmt, ht, handler]) and break if ht.match(at)
-          end
-          break unless selected.empty?
+        
+        # Set proper content-type and encoding for
+        # text based formats
+        if [:xml, :gv, :html, :json].include?(format)
+          content_type format, :charset => 'utf-8'
         end
-        selected
-      end
+        yield wants
+        # Raise this error if requested format is not defined
+        # in respond_to { } block.
+        raise MissingTemplate if wants[format].nil?
 
-      # NOTE Array instead of hash because order matters (wildcard type
-      # matches first handler)
-      class Format < Array #:nodoc:
-        def method_missing(format, *args, &handler)
-          mt = Sinatra::RespondTo::Helpers.mime_type(format)
-          if mt.nil?
-            Sinatra::Base.send(:fail, "Unknown media type for respond_to: #{format}\nTry
registering the extension with a mime type")
-          end
-          self << [format.to_s, mt, handler]
-        end
+        wants[format].call
       end
+
     end
+
   end
 end
-
-Rack::Mime::MIME_TYPES.merge!({ ".gv" => "text/plain" })
-Sinatra::Application.register Sinatra::RespondTo
diff --git a/server/server.rb b/server/server.rb
index 3fef8bf..a64e79d 100644
--- a/server/server.rb
+++ b/server/server.rb
@@ -38,6 +38,8 @@ error Deltacloud::BackendError do
   report_error(500, "backend_error")
 end
 
+Sinatra::Application.register Sinatra::RespondTo
+
 # Redirect to /api
 get '/' do redirect url_for('/api'); end
 
@@ -140,7 +142,10 @@ collection :instance_states do
         format.png do
           # Trick respond_to into looking up the right template for the
           # graphviz file
-          format(:gv); gv = erb :"instance_states/show"; format(:png)
+          format_backup = format
+          format(:gv)
+          gv = erb(:"instance_states/show")
+          format(format_backup)
           png =  ''
           cmd = 'dot -Kdot -Gpad="0.2,0.2" -Gsize="5.0,8.0" -Gdpi="180" -Tpng'
           Open3.popen3( cmd ) do |stdin, stdout, stderr|
@@ -148,6 +153,7 @@ collection :instance_states do
             stdin.close()
             png = stdout.read
           end
+          content_type 'image/png'
           png
         end
       end
-- 
1.7.2.3


Mime
View raw message