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