# frozen_string_literal: true # class Roda module RodaPlugins # Allows to respond to specific request data types. User agents can request # specific data types by either supplying an appropriate +Accept+ header # or by appending it as file extension to the path. # # Example: # # plugin :type_routing # # route do |r| # r.get 'a' do # r.html{ "

This is the HTML response

" } # r.json{ '{"json": "ok"}' } # r.xml{ "This is the XML response" } # "Unsupported data type" # end # end # # This application will handle the following paths: # /a.html :: HTML response # /a.json :: JSON response # /a.xml :: XML response # /a :: HTML, JSON, or XML response, depending on the Accept header # # The response +Content-Type+ header will be set to a suitable value when # the block is matched. # # Note that if no match is found, code will continue to execute, which can # result in unexpected behaviour. This should only happen if you do not # handle all supported/configured types. If you want to simplify handling, # you can just place the html handling after the other types, without using # a separate block: # # route do |r| # r.get 'a' do # r.json{ '{"json": "ok"}' } # r.xml{ "This is the XML response" } # # "

This is the HTML response

" # end # end # # This works correctly because Roda assumes the html type by default. # # To match custom extensions, use the :types option: # # plugin :type_routing, :types => { # :yaml => 'application/x-yaml', # :js => 'application/javascript; charset=utf-8', # } # # route do |r| # r.get 'a' do # r.yaml{ YAML.dump "YAML data" } # r.js{ "JavaScript code" } # # or: # r.on_type(:js){ "JavaScript code" } # "Unsupported data type" # end # end # # = Plugin options # # The following plugin options are supported: # # :default_type :: The default data type to assume if the client did not # provide one. Defaults to +:html+. # :exclude :: Exclude one or more types from the default set (default set # is :html, :xml, :json). # :types :: Mapping from a data type to its MIME-Type. Used both to match # incoming requests and to provide +Content-Type+ values. If the # value is +nil+, no +Content-Type+ will be set. The type may # contain media type parameters, which will be sent to the client # but ignored for request matching. # :use_extension :: Whether to take the path extension into account. # Default is +true+. # :use_header :: Whether to take the +Accept+ header into account. # Default is +true+. module TypeRouting ACCEPT_HEADER = 'HTTP_ACCEPT'.freeze CONTENT_TYPE_HEADER = 'Content-Type'.freeze CONFIGURATION = { :mimes => { 'text/json' => :json, 'application/json' => :json, 'text/xml' => :xml, 'application/xml' => :xml, 'text/html' => :html, }.freeze, :types => { :json => 'application/json'.freeze, :xml => 'application/xml'.freeze, :html => 'text/html'.freeze, }.freeze, :use_extension => true, :use_header => true, :default_type => :html }.freeze def self.configure(app, opts = {}) config = (app.opts[:type_routing] || CONFIGURATION).dup [:use_extension, :use_header, :default_type].each do |key| config[key] = opts[key] if opts.has_key?(key) end types = config[:types] = config[:types].dup mimes = config[:mimes] = config[:mimes].dup Array(opts[:exclude]).each do |type| types.delete(type) mimes.reject!{|_, v| v == type} end if mapping = opts[:types] types.merge!(mapping) mapping.each do |k, v| if v mimes[v.split(';', 2).first] = k end end end types.freeze mimes.freeze type_keys = config[:types].keys config[:extension_regexp] = /(.+)\.(#{Regexp.union(type_keys.map(&:to_s))})\z/ type_keys.each do |type| app::RodaRequest.send(:define_method, type) do |&block| on_type(type, &block) end end app.opts[:type_routing] = config.freeze end module RequestMethods # Yields if the given +type+ matches the requested data type and halts # the request afterwards, returning the result of the block. def on_type(type, &block) return unless type == requested_type response[CONTENT_TYPE_HEADER] ||= @scope.opts[:type_routing][:types][type] always(&block) end # Returns the data type the client requests. def requested_type return @requested_type if defined?(@requested_type) opts = @scope.opts[:type_routing] @requested_type = accept_response_type if opts[:use_header] @requested_type ||= opts[:default_type] end # Add the type routing extension back to the remaining path # if it was removed from the path when the application was # initialized. def run(_) if defined?(@type_routing_extension) @remaining_path += ".#{@type_routing_extension}" end super end private # Removes a trailing file extension from the path, and sets # the requested type if so. def _remaining_path(env) opts = scope.opts[:type_routing] path = super if opts[:use_extension] if m = path.match(opts[:extension_regexp]) @type_routing_extension = @requested_type = m[2].to_sym path = m[1] end end path end # The response type indicated by the Accept request header. def accept_response_type mimes = @scope.opts[:type_routing][:mimes] @env[ACCEPT_HEADER].to_s.split(/\s*,\s*/).map do |part| mime, _= part.split(/\s*;\s*/, 2) if sym = mimes[mime] return sym end end nil end end end register_plugin(:type_routing, TypeRouting) end end