module Rack
class API
class App
# Registered content types. If you want to use
# a custom formatter that is not listed here,
# you have to manually add it. Otherwise,
# Rack::API::App::DEFAULT_MIME_TYPE will be used
# as the content type.
#
MIME_TYPES = {
"json" => "application/json",
"jsonp" => "application/javascript",
"xml" => "application/xml",
"rss" => "application/rss+xml",
"atom" => "application/atom+xml",
"html" => "text/html",
"yaml" => "application/x-yaml",
"txt" => "text/plain"
}
# Default content type. Will be used when a given format
# hasn't been registered on Rack::API::App::MIME_TYPES.
#
DEFAULT_MIME_TYPE = "application/octet-stream"
attr_reader :block
attr_reader :env
# Hold block that will be executed in case the
# route is recognized.
#
def initialize(options)
options.each do |name, value|
instance_variable_set("@#{name}", value)
end
end
# Always log to the standard output.
#
def logger
@logger ||= Logger.new(STDOUT)
end
# Hold headers that will be sent on the response.
#
def headers
@headers ||= {}
end
# Merge all params into one single hash.
#
def params
@params ||= HashWithIndifferentAccess.new(request.params.merge(env["rack.routing_args"]))
end
# Return a request object.
#
def request
@request ||= Rack::Request.new(env)
end
# Return the requested format. Defaults to JSON.
#
def format
params.fetch(:format, "json")
end
# Stop processing by rendering the provided information.
#
# Rack::API.app do
# version :v1 do
# get "/" do
# error(:status => 403, :message => "Not here!")
# end
# end
# end
#
# Valid options are:
#
# # :status: a HTTP status code. Defaults to 403.
# # :message: a message that will be rendered as the response body. Defaults to "Forbidden".
# # :headers: the response headers. Defaults to {"Content-Type" => "text/plain"}.
#
# You can also provide a object that responds to to_rack. In this case, this
# method must return a valid Rack response (a 3-item array).
#
# class MyError
# def self.to_rack
# [500, {"Content-Type" => "text/plain"}, ["Internal Server Error"]]
# end
# end
#
# Rack::API.app do
# version :v1 do
# get "/" do
# error(MyError)
# end
# end
# end
#
def error(options = {})
throw :error, Response.new(options)
end
# Set response status code.
#
def status(*args)
@status = args.first unless args.empty?
@status || 200
end
# Reset environment between requests.
#
def reset! # :nodoc:
@params = nil
@request = nil
@headers = nil
end
# Return credentials for Basic Authentication request.
#
def credentials
@credentials ||= begin
request = Rack::Auth::Basic::Request.new(env)
request.provided? ? request.credentials : []
end
end
# Render the result of block.
#
def call(env) # :nodoc:
reset!
@env = env
response = catch(:error) do
render instance_eval(&block)
end
response.respond_to?(:to_rack) ? response.to_rack : response
end
# Return response content type based on extension.
# If you're using an unknown extension that wasn't registered on
# Rack::API::App::MIME_TYPES, it will return Rack::API::App::DEFAULT_MIME_TYPE,
# which defaults to application/octet-stream.
#
def content_type
mime = MIME_TYPES.fetch(format, DEFAULT_MIME_TYPE)
headers.fetch("Content-Type", mime)
end
private
def render(response) # :nodoc:
[status, headers.merge("Content-Type" => content_type), [format_response(response)]]
end
def format_response(response) # :nodoc:
formatter_name = format.split("_").collect {|word| word[0,1].upcase + word[1,word.size].downcase}.join("")
if Rack::API::Formatter.const_defined?(formatter_name)
formatter = Rack::API::Formatter.const_get(formatter_name).new(response, params)
formatter.to_format
elsif response.respond_to?("to_#{format}")
response.__send__("to_#{format}")
else
throw :error, Response.new(:status => 406, :message => "Unknown format")
end
end
end
end
end