lib/grape/api.rb in grape-0.1.5 vs lib/grape/api.rb in grape-0.2.0
- old
+ new
@@ -3,51 +3,73 @@
require 'rack/auth/digest/md5'
require 'logger'
module Grape
# The API class is the primary entry point for
- # creating Grape APIs. Users should subclass this
+ # creating Grape APIs.Users should subclass this
# class in order to build an API.
class API
class << self
attr_reader :route_set
-
- def logger
- @logger ||= Logger.new($STDOUT)
+ attr_reader :versions
+ attr_reader :routes
+ attr_reader :settings
+ attr_writer :logger
+ attr_reader :endpoints
+ attr_reader :mountings
+ attr_reader :instance
+
+ def logger(logger = nil)
+ if logger
+ @logger = logger
+ else
+ @logger ||= Logger.new($stdout)
+ end
end
-
+
def reset!
- @settings = [{}]
+ @settings = Grape::Util::HashStack.new
@route_set = Rack::Mount::RouteSet.new
- @prototype = nil
+ @endpoints = []
+ @mountings = []
+ @routes = nil
end
-
+
+ def compile
+ @instance = self.new
+ end
+
+ def change!
+ @instance = nil
+ end
+
def call(env)
- logger.info "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
- route_set.freeze.call(env)
+ compile unless instance
+ call!(env)
end
-
- # Settings are a stack, so when we
- # want to access them they are merged
- # in the order pushed.
- def settings
- @settings.inject({}){|f,h| f.merge!(h); f}
+
+ def call!(env)
+ instance.call(env)
end
-
- def settings_stack
- @settings
+
+ # Set a configuration value for this namespace.
+ #
+ # @param key [Symbol] The key of the configuration variable.
+ # @param value [Object] The value to which to set the configuration variable.
+ def set(key, value)
+ settings[key.to_sym] = value
end
-
- # Set a configuration value for this
+
+ # Add to a configuration value for this
# namespace.
#
# @param key [Symbol] The key of the configuration variable.
# @param value [Object] The value to which to set the configuration variable.
- def set(key, value)
- @settings.last[key.to_sym] = value
+ def imbue(key, value)
+ settings.imbue(key, value)
end
-
+
# Define a root URL prefix for your entire
# API.
def prefix(prefix = nil)
prefix ? set(:root_prefix, prefix) : settings[:root_prefix]
end
@@ -55,39 +77,64 @@
# Specify an API version.
#
# @example API with legacy support.
# class MyAPI < Grape::API
# version 'v2'
- #
+ #
# get '/main' do
# {:some => 'data'}
# end
- #
+ #
# version 'v1' do
# get '/main' do
# {:legacy => 'data'}
# end
# end
# end
#
- def version(*new_versions, &block)
- new_versions.any? ? nest(block){ set(:version, new_versions) } : settings[:version]
+ def version(*args, &block)
+ if args.any?
+ options = args.pop if args.last.is_a? Hash
+ options ||= {}
+ options = {:using => :path}.merge!(options)
+ @versions = versions | args
+ nest(block) do
+ set(:version, args)
+ set(:version_options, options)
+ end
+ end
end
-
- # Specify the default format for the API's
- # serializers. Currently only `:json` is
- # supported.
+
+ # Add a description to the next namespace or function.
+ def desc(description, options = {})
+ @last_description = options.merge(:description => description)
+ end
+
+ # Specify the default format for the API's serializers.
+ # May be `:json` or `:txt` (default).
def default_format(new_format = nil)
new_format ? set(:default_format, new_format.to_sym) : settings[:default_format]
end
+ # Specify the format for the API's serializers.
+ # May be `:json` or `:txt`.
+ def format(new_format = nil)
+ new_format ? set(:format, new_format.to_sym) : settings[:format]
+ end
+
# Specify the format for error messages.
# May be `:json` or `:txt` (default).
def error_format(new_format = nil)
new_format ? set(:error_format, new_format.to_sym) : settings[:error_format]
end
+ # Specify additional content-types, e.g.:
+ # content_type :xls, 'application/vnd.ms-excel'
+ def content_type(key, val)
+ settings.imbue(:content_types, key.to_sym => val)
+ end
+
# Specify the default status code for errors.
def default_error_status(new_status = nil)
new_status ? set(:default_error_status, new_status) : settings[:default_error_status]
end
@@ -103,53 +150,91 @@
# end
#
# @overload rescue_from(*exception_classes, options = {})
# @param [Array] exception_classes A list of classes that you want to rescue, or
# the symbol :all to rescue from all exceptions.
+ # @param [Block] block Execution block to handle the given exception.
# @param [Hash] options Options for the rescue usage.
# @option options [Boolean] :backtrace Include a backtrace in the rescue response.
- def rescue_from(*args)
- set(:rescue_options, args.pop) if args.last.is_a?(Hash)
+ def rescue_from(*args, &block)
+ if block_given?
+ args.each do |arg|
+ imbue(:rescue_handlers, { arg => block })
+ end
+ end
+ imbue(:rescue_options, args.pop) if args.last.is_a?(Hash)
set(:rescue_all, true) and return if args.include?(:all)
- set(:rescued_errors, args)
+ imbue(:rescued_errors, args)
end
+ # Allows you to specify a default representation entity for a
+ # class. This allows you to map your models to their respective
+ # entities once and then simply call `present` with the model.
+ #
+ # @example
+ # class ExampleAPI < Grape::API
+ # represent User, :with => Entity::User
+ #
+ # get '/me' do
+ # present current_user # :with => Entity::User is assumed
+ # end
+ # end
+ #
+ # Note that Grape will automatically go up the class ancestry to
+ # try to find a representing entity, so if you, for example, define
+ # an entity to represent `Object` then all presented objects will
+ # bubble up and utilize the entity provided on that `represent` call.
+ #
+ # @param model_class [Class] The model class that will be represented.
+ # @option options [Class] :with The entity class that will represent the model.
+ def represent(model_class, options)
+ raise ArgumentError, "You must specify an entity class in the :with option." unless options[:with] && options[:with].is_a?(Class)
+ imbue(:representations, model_class => options[:with])
+ end
+
# Add helper methods that will be accessible from any
# endpoint within this namespace (and child namespaces).
#
+ # When called without a block, all known helpers within this scope
+ # are included.
+ #
+ # @param mod [Module] optional module of methods to include
+ # @param &block [Block] optional block of methods to include
+ #
# @example Define some helpers.
# class ExampleAPI < Grape::API
# helpers do
# def current_user
# User.find_by_id(params[:token])
# end
# end
# end
- def helpers(&block)
- if block_given?
- m = settings_stack.last[:helpers] || Module.new
- m.class_eval &block
- set(:helpers, m)
+ def helpers(mod = nil, &block)
+ if block_given? || mod
+ mod ||= settings.peek[:helpers] || Module.new
+ mod.class_eval &block if block_given?
+ set(:helpers, mod)
else
- m = Module.new
- settings_stack.each do |s|
- m.send :include, s[:helpers] if s[:helpers]
+ mod = Module.new
+ settings.stack.each do |s|
+ mod.send :include, s[:helpers] if s[:helpers]
end
- m
+ change!
+ mod
end
end
-
+
# Add an authentication type to the API. Currently
# only `:http_basic`, `:http_digest` and `:oauth2` are supported.
def auth(type = nil, options = {}, &block)
if type
set(:auth, {:type => type.to_sym, :proc => block}.merge(options))
else
settings[:auth]
end
end
-
+
# Add HTTP Basic authorization to the API.
#
# @param [Hash] options A hash of options.
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
def http_basic(options = {}, &block)
@@ -157,14 +242,29 @@
auth :http_basic, options, &block
end
def http_digest(options = {}, &block)
options[:realm] ||= "API Authorization"
- options[:opaque] ||= "secret"
+ options[:opaque] ||= "secret"
auth :http_digest, options, &block
end
-
+
+ def mount(mounts)
+ mounts = {mounts => '/'} unless mounts.respond_to?(:each_pair)
+ mounts.each_pair do |app, path|
+ if app.respond_to?(:inherit_settings)
+ app.inherit_settings(settings.clone)
+ end
+
+ endpoints << Grape::Endpoint.new(settings.clone,
+ :method => :any,
+ :path => path,
+ :app => app
+ )
+ end
+ end
+
# Defines a route that will be recognized
# by the Grape API.
#
# @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
# @param paths [String] One or more strings representing the URL segment(s) for this route.
@@ -173,129 +273,132 @@
# class MyAPI < Grape::API
# route(:any, '/hello') do
# {:hello => 'world'}
# end
# end
- def route(methods, paths, &block)
- methods = Array(methods)
- paths = ['/'] if paths == []
- paths = Array(paths)
- endpoint = build_endpoint(&block)
- options = {}
- options[:version] = /#{version.join('|')}/ if version
-
- methods.each do |method|
- paths.each do |path|
- path = Rack::Mount::Strexp.compile(compile_path(path), options, ['/'], true)
- route_set.add_route(endpoint,
- :path_info => path,
- :request_method => (method.to_s.upcase unless method == :any)
- )
- end
- end
+ def route(methods, paths = ['/'], route_options = {}, &block)
+ endpoint_options = {
+ :method => methods,
+ :path => paths,
+ :route_options => (route_options || {}).merge(@last_description || {})
+ }
+ endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)
+ @last_description = nil
end
-
- def get(*paths, &block); route('GET', paths, &block) end
- def post(*paths, &block); route('POST', paths, &block) end
- def put(*paths, &block); route('PUT', paths, &block) end
- def head(*paths, &block); route('HEAD', paths, &block) end
- def delete(*paths, &block); route('DELETE', paths, &block) end
-
+
+ def before(&block)
+ imbue(:befores, [block])
+ end
+
+ def after(&block)
+ imbue(:afters, [block])
+ end
+
+ def get(paths = ['/'], options = {}, &block); route('GET', paths, options, &block) end
+ def post(paths = ['/'], options = {}, &block); route('POST', paths, options, &block) end
+ def put(paths = ['/'], options = {}, &block); route('PUT', paths, options, &block) end
+ def head(paths = ['/'], options = {}, &block); route('HEAD', paths, options, &block) end
+ def delete(paths = ['/'], options = {}, &block); route('DELETE', paths, options, &block) end
+ def options(paths = ['/'], options = {}, &block); route('OPTIONS', paths, options, &block) end
+ def patch(paths = ['/'], options = {}, &block); route('PATCH', paths, options, &block) end
+
def namespace(space = nil, &block)
if space || block_given?
nest(block) do
set(:namespace, space.to_s) if space
end
else
- Rack::Mount::Utils.normalize_path(settings_stack.map{|s| s[:namespace]}.join('/'))
+ Rack::Mount::Utils.normalize_path(settings.stack.map{|s| s[:namespace]}.join('/'))
end
end
-
+
alias_method :group, :namespace
alias_method :resource, :namespace
alias_method :resources, :namespace
-
+ alias_method :segment, :namespace
+
# Create a scope without affecting the URL.
- #
+ #
# @param name [Symbol] Purely placebo, just allows to to name the scope to make the code more readable.
def scope(name = nil, &block)
nest(block)
end
# Apply a custom middleware to the API. Applies
# to the current namespace and any children, but
# not parents.
#
- # @param middleware_class [Class] The class of the middleware you'd like to inject.
- def use(middleware_class, *args)
- settings_stack.last[:middleware] ||= []
- settings_stack.last[:middleware] << [middleware_class, *args]
+ # @param middleware_class [Class] The class of the middleware you'd like
+ # to inject.
+ def use(middleware_class, *args, &block)
+ arr = [middleware_class, *args]
+ arr << block if block_given?
+ imbue(:middleware, [arr])
end
# Retrieve an array of the middleware classes
# and arguments that are currently applied to the
# application.
def middleware
- settings_stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
+ settings.stack.inject([]){|a,s| a += s[:middleware] if s[:middleware]; a}
end
+ # An array of API routes.
+ def routes
+ @routes ||= prepare_routes
+ end
+
+ def versions
+ @versions ||= []
+ end
+
protected
-
+
+ def prepare_routes
+ routes = []
+ endpoints.each do |endpoint|
+ routes.concat(endpoint.routes)
+ end
+ routes
+ end
+
# Execute first the provided block, then each of the
# block passed in. Allows for simple 'before' setups
# of settings stack pushes.
def nest(*blocks, &block)
blocks.reject!{|b| b.nil?}
if blocks.any?
- settings_stack << {}
+ settings.push # create a new context to eval the follow
instance_eval &block if block_given?
blocks.each{|b| instance_eval &b}
- settings_stack.pop
+ settings.pop # when finished, we pop the context
else
instance_eval &block
end
end
-
- def build_endpoint(&block)
- b = Rack::Builder.new
- b.use Grape::Middleware::Error,
- :default_status => settings[:default_error_status] || 403,
- :rescue_all => settings[:rescue_all],
- :rescued_errors => settings[:rescued_errors],
- :format => settings[:error_format] || :txt,
- :rescue_options => settings[:rescue_options]
- b.use Rack::Auth::Basic, settings[:auth][:realm], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_basic
- b.use Rack::Auth::Digest::MD5, settings[:auth][:realm], settings[:auth][:opaque], &settings[:auth][:proc] if settings[:auth] && settings[:auth][:type] == :http_digest
- b.use Grape::Middleware::Prefixer, :prefix => prefix if prefix
- b.use Grape::Middleware::Versioner, :versions => (version if version.is_a?(Array)) if version
- b.use Grape::Middleware::Formatter, :default_format => default_format || :json
- middleware.each{|m| b.use *m }
- endpoint = Grape::Endpoint.generate(&block)
- endpoint.send :include, helpers
- b.run endpoint
-
- b.to_app
- end
-
- def inherited(subclass)
+ def inherited(subclass)
subclass.reset!
+ subclass.logger = logger.clone
end
-
- def route_set
- @route_set ||= Rack::Mount::RouteSet.new
+
+ def inherit_settings(other_stack)
+ settings.prepend other_stack
+ endpoints.each{|e| e.settings.prepend(other_stack)}
end
-
- def compile_path(path)
- parts = []
- parts << prefix if prefix
- parts << ':version' if version
- parts << namespace.to_s if namespace
- parts << path.to_s if path && '/' != path
- parts.last << '(.:format)'
- Rack::Mount::Utils.normalize_path(parts.join('/'))
+ end
+
+ def initialize
+ @route_set = Rack::Mount::RouteSet.new
+ self.class.endpoints.each do |endpoint|
+ endpoint.mount_in(@route_set)
end
- end
-
- reset!
+ @route_set.freeze
+ end
+
+ def call(env)
+ @route_set.call(env)
+ end
+
+ reset!
end
end