module Rack class API class Runner HTTP_METHODS = %w[get post put delete head] DELEGATE_METHODS = %w[ version use prefix basic_auth helper respond_to default_url_options ] attr_accessor :settings def initialize @settings = { :middlewares => [], :helpers => [], :global => { :prefix => "/", :formats => %w[json jsonp], :middlewares => [], :helpers => [] } } end # Set configuration based on scope. When defining values outside version block, # will set configuration using settings[:global] namespace. # # Use the Rack::API::Runner#option method to access a given setting. # def set(name, value, mode = :override) target = settings[:version] ? settings : settings[:global] if mode == :override target[name] = value else target[name] << value end end # Try to fetch local configuration, defaulting to the global setting. # Return +nil+ when no configuration is defined. # def option(name, mode = :any) if mode == :merge && (settings[name].kind_of?(Array) || settings[:global][name].kind_of?(Array)) settings[:global].fetch(name, []) | settings.fetch(name, []) else settings.fetch(name, settings[:global][name]) end end # Add a middleware to the execution stack. # # Global middlewares will be merged with local middlewares. # # Rack::API.app do # use ResponseTime # # version :v1 do # use Gzip # end # end # # The middleware stack will be something like [ResponseTime, Gzip]. # def use(middleware, *args) set :middlewares, [middleware, *args], :append end # Set an additional url prefix. # def prefix(name) set :prefix, name end # Add a helper to application. # # helper MyHelpers # helper { } # def helper(mod = nil, &block) mod = Module.new(&block) if block_given? raise ArgumentError, "you need to pass a module or block" unless mod set :helpers, mod, :append end # Define the server endpoint. Will be used if you call the method # Rack::API::App#url_for. # # The following options are supported: # # * :host – Specifies the host the link should be targeted at. # * :protocol – The protocol to connect to. Defaults to 'http'. # * :port – Optionally specify the port to connect to. # * :base_path – Optionally specify a base path. # # default_url_options :host => "myhost.com" # #=> http://myhost.com # # default_url_options :host => "myhost.com", :protocol => "https" # #=> https://myhost.com # # default_url_options :host => "myhost.com", :port => 3000 # #=> http://myhost.com:3000 # # default_url_options :host => "myhost.com", :base_path => "my/custom/path" # #=> http://myhost.com/my/custom/path # def default_url_options(options) set :url_options, options end # Create a new API version. # def version(name, &block) raise ArgumentError, "you need to pass a block" unless block_given? settings[:version] = name.to_s instance_eval(&block) settings.delete(:version) end # Run all routes. # def call(env) # :nodoc: route_set.freeze.call(env) end # Require basic authentication before processing other requests. # The authentication reques must be defined before routes. # # Rack::API.app do # basic_auth "Protected Area" do |user, pass| # User.authenticate(user, pass) # end # end # # You can disable basic authentication by providing :none as # realm. # # Rack::API.app do # basic_auth "Protected Area" do |user, pass| # User.authenticate(user, pass) # end # # version :v1 do # # this version is protected by the # # global basic auth block above. # end # # version :v2 do # basic_auth :none # # this version is now public # end # # version :v3 do # basic_auth "Admin" do |user, pass| # user == "admin" && pass == "test" # end # end # end # def basic_auth(realm = "Restricted Area", &block) set :auth, (realm == :none ? :none : [realm, block]) end # Define the formats that this app implements. # Respond only to :json by default. # # When setting a format you have some alternatives on how this object # will be formated. # # First, Rack::API will look for a formatter defined on Rack::API::Formatter # namespace. If there's no formatter, it will look for a method to_. # It will raise an exception if no formatter method has been defined. # # See Rack::API::Formatter::Jsonp for an example on how to create additional # formatters. # # Local formats will override the global configuration on that context. # # Rack::API.app do # respond_to :json, :xml, :jsonp # # version :v1 do # respond_to :json # end # end # # The code above will accept only :json as format on version :v1. # # Also, the first value provided to this method will be used as default format, # which means that requests that don't provide the :format param, will use # this value. # # respond_to :fffuuu, :json # #=> the default format is fffuuu # def respond_to(*formats) set :formats, formats end # Hold all routes. # def route_set # :nodoc: @route_set ||= Rack::Mount::RouteSet.new end # Define a new routing that will be triggered when both request method and # path are recognized. # # You're better off using all verb shortcut methods. Implemented verbs are # +get+, +post+, +put+, +delete+ and +head+. # # class MyAPI < Rack::API # version "v1" do # get "users(.:format)" do # # do something # end # end # end # def route(method, path, requirements = {}, &block) path = Rack::Mount::Strexp.compile mount_path(path), requirements, %w[ / . ? ] route_set.add_route(build_app(block), :path_info => path, :request_method => method) end HTTP_METHODS.each do |method| class_eval <<-RUBY, __FILE__, __LINE__ def #{method}(*args, &block) # def get(*args, &block) route("#{method.upcase}", *args, &block) # route("GET", *args, &block) end # end RUBY end private def mount_path(path) # :nodoc: Rack::Mount::Utils.normalize_path([option(:prefix), settings[:version], path].join("/")) end def default_format # :nodoc: (option(:formats).first || "json").to_s end def build_app(handler) # :nodoc: app = App.new({ :handler => handler, :default_format => default_format, :version => option(:version), :prefix => option(:prefix), :url_options => option(:url_options) }) builder = Rack::Builder.new # Add middleware for basic authentication. auth = option(:auth) builder.use Rack::Auth::Basic, auth[0], &auth[1] if auth && auth != :none # Add middleware for format validation. builder.use Rack::API::Middleware::Format, default_format, option(:formats) # Add middlewares to executation stack. option(:middlewares, :merge).each {|middleware| builder.use(*middleware)} # Apply helpers to app. helpers = option(:helpers) app.extend *helpers unless helpers.empty? builder.run(app) builder.to_app end end end end