lib/roda/plugins/status_handler.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The status_handler plugin adds a +status_handler+ method which sets a
    # block that is called whenever a response with the relevant response code
    # with an empty body would be returned.
    #
    # This plugin does not support providing the blocks with the plugin call;
    # you must provide them to status_handler calls afterwards:
    #
    #   plugin :status_handler
    #
    #   status_handler(403) do
    #     "You are forbidden from seeing that!"
    #   end
    #
    #   status_handler(404) do
    #     "Where did it go?"
    #   end
    #
    #   status_handler(405, keep_headers: ['Accept']) do
    #     "Use a different method!"
    #   end
    #
    # Before a block is called, any existing headers on the response will be
    # cleared, unless the +:keep_headers+ option is used.  If the +:keep_headers+
    # option is used, the value should be an array, and only the headers listed
    # in the array will be kept.
    module StatusHandler
      CLEAR_HEADERS = :clear.to_proc
      private_constant :CLEAR_HEADERS

      def self.configure(app)
        app.opts[:status_handler] ||= {}
      end

      module ClassMethods
        # Install the given block as a status handler for the given HTTP response code.
        def status_handler(code, opts=OPTS, &block)
          # For backwards compatibility, pass request argument if block accepts argument
          arity = block.arity == 0 ? 0 : 1
          handle_headers = case keep_headers = opts[:keep_headers]
          when nil, false
            CLEAR_HEADERS
          when Array
            if Rack.release >= '3'
              keep_headers = keep_headers.map(&:downcase)
            end
            lambda{|headers| headers.delete_if{|k,_| !keep_headers.include?(k)}}
          else
            raise RodaError, "Invalid :keep_headers option"
          end

          meth = define_roda_method(:"_roda_status_handler__#{code}", arity, &block)
          self.opts[:status_handler][code] = define_roda_method(:"_roda_status_handler_#{code}", 1) do |result|
            res = @_response
            res.status = result[0]
            handle_headers.call(res.headers)
            result.replace(_roda_handle_route{arity == 1 ? send(meth, @_request) : send(meth)})
          end
        end

        # Freeze the hash of status handlers so that there can be no thread safety issues at runtime.
        def freeze
          opts[:status_handler].freeze
          super
        end
      end

      module InstanceMethods
        private

        # If routing returns a response we have a handler for, call that handler.
        def _roda_after_20__status_handler(result)
          if result && (meth = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
            send(meth, result)
          end
        end
      end
    end

    register_plugin(:status_handler, StatusHandler)
  end
end