require 'http_router' require 'goliath/goliath' require 'goliath/response' require 'goliath/request' require 'goliath/rack' require 'goliath/validation' module Goliath # All Goliath APIs subclass Goliath::API. All subclasses _must_ override the # {#response} method. # # @example # require 'goliath' # # class HelloWorld < Goliath::API # def response(env) # [200, {}, "hello world"] # end # end # class API include Goliath::Constants include Goliath::Rack::Validator class << self # Catches the userland class which inherits the Goliath API # # In case of further subclassing, the very last class encountered is used. def inherited(subclass) Goliath::Application.app_class = subclass.name if defined?(Goliath::Application) end # Retrieves the middlewares defined by this API server # # @return [Array] array contains [middleware class, args, block] def middlewares @middlewares ||= [] unless @loaded_default_middlewares @middlewares.unshift([::Goliath::Rack::DefaultResponseFormat, nil, nil]) @middlewares.unshift([::Rack::ContentLength, nil, nil]) if Goliath.dev? && !@middlewares.detect {|mw| mw.first == ::Rack::Reloader} @middlewares.unshift([::Rack::Reloader, 0, nil]) end @loaded_default_middlewares = true end @middlewares end # Specify a middleware to be used by the API # # @example # use Goliath::Rack::Validation::RequiredParam, {:key => 'echo'} # # use ::Rack::Rewrite do # rewrite %r{^(.*?)\??gziped=(.*)$}, lambda { |match, env| "#{match[1]}?echo=#{match[2]}" } # end # # @param name [Class] The middleware class to use # @param args Any arguments to pass to the middeware # @param block A block to pass to the middleware def use(name, *args, &block) @middlewares ||= [] if name == Goliath::Rack::Render [args].flatten.each do |type| type = Goliath::Rack::Formatters.const_get type.upcase @middlewares << [type, nil, nil] end end @middlewares << [name, args, block] end # Returns the plugins configured for this API # # @return [Array] array contains [plugin name, args] def plugins @plugins ||= [] end # Specify a plugin to be used by the API # # @example # plugin Goliath::Plugin::Latency # # @param name [Class] The plugin class to use # @param args The arguments to the plugin def plugin(name, *args) plugins.push([name, args]) end # Returns the router maps configured for the API # # @return [Array] array contains [path, klass, block] def maps @maps ||= [] end def maps? !maps.empty? end # Specify a router map to be used by the API # # @example # map '/version' do # run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] } # end # # @example # map '/user/:id', :id => /\d+/ do # # params[:id] will be a number # run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Loading user #{params[:id]}"]] } # end # # @param name [String] The URL path to map. # Optional parts are supported via (.:format), variables as :var and globs via *remaining_path. # Variables can be validated by supplying a Regexp. # @param klass [Class] The class to retrieve the middlewares from # @param block The code to execute def map(name, *args, &block) opts = args.last.is_a?(Hash) ? args.pop : {} klass = args.first if klass && block_given? raise "Can't provide class and block to map" end maps.push([name, klass, opts, block]) end [:get, :post, :head, :put, :delete].each do |http_method| class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{http_method}(name, *args, &block) opts = args.last.is_a?(Hash) ? args.pop : {} klass = args.first opts[:conditions] ||= {} opts[:conditions][:request_method] = [#{http_method == :get ? "'HEAD', 'GET'" : http_method.to_s.upcase.inspect}] map(name, klass, opts, &block) end EOT end def router unless @router @router = HttpRouter.new @router.default(proc{ |env| @router.routes.last.dest.call(env) }) end @router end # Use to define the 404 routing logic. As well, define any number of other paths to also run the not_found block. def not_found(*other_paths, &block) app = ::Rack::Builder.new(&block).to_app router.default(app) other_paths.each {|path| router.add(path).to(app) } end end # Default stub method to add options into the option parser. # # @example # def options_parser(opts, options) # options[:test] = 0 # opts.on('-t', '--test NUM', "The test number") { |val| options[:test] = val.to_i } # end # # @param opts [OptionParser] The options parser # @param options [Hash] The hash to insert the parsed options into def options_parser(opts, options) end # Accessor for the current env object # # @note This will not work in a streaming server. You must pass around the env object. # # @return [Goliath::Env] The current environment data for the request def env Thread.current[GOLIATH_ENV] end # The API will proxy missing calls to the env object if possible. # # The two entries in this example are equivalent as long as you are not # in a streaming server. # # @example # logger.info "Hello" # env.logger.info "Hello" def method_missing(name, *args, &blk) name = name.to_s if env.respond_to?(name) env.send(name, *args, &blk) else super(name.to_sym, *args, &blk) end end # @param name [Symbol] The method to check if we respond to it. # @return [Boolean] True if the API's method_missing responds to the method def respond_to_missing?(name, *) env.respond_to? name end # {#call} is executed automatically by the middleware chain and will setup # the environment for the {#response} method to execute. This includes setting # up a new Fiber, handing any exceptions thrown from the API and executing # the appropriate callback method for the API. # # @param env [Goliath::Env] The request environment # @return [Goliath::Connection::AsyncResponse] An async response. def call(env) begin Thread.current[GOLIATH_ENV] = env status, headers, body = response(env) if status if body == Goliath::Response::STREAMING env[STREAM_START].call(status, headers) else env[ASYNC_CALLBACK].call([status, headers, body]) end end rescue Goliath::Validation::Error => e env[RACK_EXCEPTION] = e env[ASYNC_CALLBACK].call(validation_error(e.status_code, e.message)) rescue Exception => e env.logger.error(e.message) env.logger.error(e.backtrace.join("\n")) env[RACK_EXCEPTION] = e env[ASYNC_CALLBACK].call(validation_error(500, e.message)) end Goliath::Connection::AsyncResponse end # Response is the main implementation method for Goliath APIs. All APIs # should override this method in order to do any actual work. # # The response method will be executed in a new Fiber and wrapped in a # begin rescue block to handle an thrown API errors. # # @param env [Goliath::Env] The request environment # @return [Array] Array contains [Status code, Headers Hash, Body] def response(env) env.logger.error('You need to implement response') raise Goliath::Validation::InternalServerError.new('No response implemented') end # Helper method for streaming response apis. # # @param status_code [Integer] The status code to return (200 by default). # @param headers [Hash] Headers to return. def streaming_response(status_code = 200, headers = {}) [status_code, headers, Goliath::Response::STREAMING] end # Helper method for chunked transfer streaming response apis # # Chunked transfer streaming is transparent to all clients (it's just as # good as a normal response), but allows an aware client to begin consuming # the stream even as it's produced. # # * http://en.wikipedia.org/wiki/Chunked_transfer_encoding # * http://developers.sun.com/mobility/midp/questions/chunking/ # * http://blog.port80software.com/2006/11/08/ # # @param status_code [Integer] The status code to return. # @param headers [Hash] Headers to return. The Transfer-Encoding=chunked # header is set for you. # # If you are using chunked streaming, you must use # env.chunked_stream_send and env.chunked_stream_close def chunked_streaming_response(status_code = 200, headers = {}) streaming_response(status_code, headers.merge(Goliath::Response::CHUNKED_STREAM_HEADERS)) end end end