module Rack
class API
class Controller
# 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::Controller::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::Controller::MIME_TYPES.
#
DEFAULT_MIME_TYPE = "application/octet-stream"
# Hold block that will be executed in case the
# route is recognized.
#
attr_accessor :handler
# Hold environment from current request.
#
attr_accessor :env
# Define which will be the default format when format=
# is not defined.
attr_accessor :default_format
# Set the default prefix path.
#
attr_accessor :prefix
# Specify the API version.
#
attr_accessor :version
# Hold url options.
#
attr_accessor :url_options
# Hold handlers, that will wrap exceptions
# into a normalized response.
#
attr_accessor :rescuers
def initialize(options = {})
options.each do |name, value|
instance_variable_set("@#{name}", value)
end
@url_options ||= {}
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, default_format)
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 handler.
#
def call(env) # :nodoc:
reset!
@env = env
response = catch(:error) do
render instance_eval(&handler)
end
response.respond_to?(:to_rack) ? response.to_rack : response
rescue Exception => exception
handle_exception exception
end
# Return response content type based on extension.
# If you're using an unknown extension that wasn't registered on
# Rack::API::Controller::MIME_TYPES, it will return Rack::API::Controller::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
# Return a URL path for all segments.
# You can set default options by using the
# Rack::API::Runner#default_url_options method.
#
# url_for :users
# #=> /users
#
# url_for :users, User.first
# #=> /users/1
#
# url_for :users, 1, :format => :json
# #=> /users/1?format=json
#
# url_for :users, :filters => [:name, :age]
# #=> /users?filters[]=name&filters[]=age
#
# URL segments can be any kind of object. First it'll be checked if it responds to
# the to_param method. If not, converts object to string by using the
# to_s method.
#
def url_for(*args)
options = {}
options = args.pop if args.last.kind_of?(Hash)
segments = []
segments << url_options[:base_path] if url_options[:base_path]
segments << prefix if prefix
segments << version
segments += args.collect {|part| part.respond_to?(:to_param) ? part.to_param : part.to_s }
url = ""
url << url_options.fetch(:protocol, "http").to_s << "://"
url << url_options.fetch(:host, env["SERVER_NAME"])
port = url_options.fetch(:port, env["SERVER_PORT"]).to_i
url << ":" << port.to_s if port.nonzero? && port != 80
url << Rack::Mount::Utils.normalize_path(segments.join("/"))
url << "?" << options.to_param if options.any?
url
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, env, 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
def handle_exception(error) # :nodoc:
rescuer = rescuers.find do |r|
error_class = eval("::#{r[:class_name]}") rescue nil
error_class && error.kind_of?(error_class)
end
raise error unless rescuer
if rescuer[:block]
instance_exec(error, &rescuer[:block])
else
[rescuer[:options].fetch(:status, 500), {"Content-Type" => "text/plain"}, []]
end
end
end
end
end