module Jellyfish autoload :VERSION , 'jellyfish/version' autoload :Sinatra , 'jellyfish/sinatra' autoload :NewRelic, 'jellyfish/newrelic' autoload :MultiActions , 'jellyfish/multi_actions' autoload :NormalizedParams, 'jellyfish/normalized_params' autoload :NormalizedPath , 'jellyfish/normalized_path' autoload :ChunkedBody, 'jellyfish/chunked_body' GetValue = Object.new Identity = lambda{|_|_} class Response < RuntimeError def headers @headers ||= {'Content-Type' => 'text/html'} end def body @body ||= [File.read("#{Jellyfish.public_root}/#{status}.html")] end end class InternalError < Response; def status; 500; end; end class NotFound < Response; def status; 404; end; end class Found < Response # this would be raised in redirect attr_reader :url def initialize url; @url = url ; end def status ; 302 ; end def headers ; super.merge('Location' => url) ; end def body ; super.map{ |b| b.gsub('VAR_URL', url) }; end end # ----------------------------------------------------------------- class Controller attr_reader :routes, :jellyfish, :env def initialize routes, jellyfish @routes, @jellyfish = routes, jellyfish @status, @headers, @body = nil end def call env @env = env block_call(*dispatch) end def block_call argument, block val = instance_exec(argument, &block) [status || 200, headers || {}, body || with_each(val || '')] rescue LocalJumpError jellyfish.log("Use `next' if you're trying to `return' or" \ " `break' from the block.", env['rack.errors']) raise end def request ; @request ||= Rack::Request.new(env); end def halt *args; throw(:halt, *args) ; end def forward ; halt(Jellyfish::NotFound.new) ; end def found url; halt(Jellyfish:: Found.new(url)); end alias_method :redirect, :found def path_info ; env['PATH_INFO'] || '/' ; end def request_method; env['REQUEST_METHOD'] || 'GET'; end %w[status headers].each do |field| module_eval <<-RUBY def #{field} value=GetValue if value == GetValue @#{field} else @#{field} = value end end RUBY end def body value=GetValue if value == GetValue @body elsif value.nil? @body = value else @body = with_each(value) end end def headers_merge value if headers.nil? headers(value) else headers(headers.merge(value)) end end private def actions routes[request_method.downcase] || halt(Jellyfish::NotFound.new) end def dispatch actions.find{ |(route, block)| case route when String break route, block if route == path_info else#Regexp, using else allows you to use custom matcher match = route.match(path_info) break match, block if match end } || halt(Jellyfish::NotFound.new) end def with_each value if value.respond_to?(:each) then value else [value] end end end # ----------------------------------------------------------------- module DSL def routes ; @routes ||= {}; end def handlers; @handlers ||= {}; end def handle exception, █ handlers[exception] = block; end def handle_exceptions value=GetValue if value == GetValue @handle_exceptions else @handle_exceptions = value end end def controller_include *value (@controller_include ||= []).push(*value) end def controller value=GetValue if value == GetValue @controller ||= controller_inject( const_set(:Controller, Class.new(Controller))) else @controller = controller_inject(value) end end def controller_inject value controller_include. inject(value){ |ctrl, mod| ctrl.__send__(:include, mod) } end %w[options get head post put delete patch].each do |method| module_eval <<-RUBY def #{method} route=//, &block raise TypeError.new("Route \#{route} should respond to :match") \ unless route.respond_to?(:match) (routes['#{method}'] ||= []) << [route, block] end RUBY end def inherited sub sub.handle_exceptions(handle_exceptions) sub.controller_include(*controller_include) [:handlers, :routes].each{ |m| val = __send__(m).inject({}){ |r, (k, v)| r[k] = v.dup; r } sub.__send__(m).replace(val) # dup the routing arrays } end end # ----------------------------------------------------------------- def initialize app=nil; @app = app; end def call env ctrl = self.class.controller.new(self.class.routes, self) case res = catch(:halt){ ctrl.call(env) } when NotFound app && forward(ctrl, env) || handle(ctrl, res) when Response handle(ctrl, res, env['rack.errors']) else res || ctrl.block_call(nil, Identity) end rescue Exception => e handle(ctrl, e, env['rack.errors']) end def log_error e, stderr return unless stderr stderr.puts("[#{self.class.name}] #{e.inspect} #{e.backtrace}") end def log msg, stderr return unless stderr stderr.puts("[#{self.class.name}] #{msg}") end private def forward ctrl, env app.call(env) rescue Exception => e handle(ctrl, e, env['rack.errors']) end def handle ctrl, e, stderr=nil if handler = best_handler(e) ctrl.block_call(e, handler) elsif !self.class.handle_exceptions raise e elsif e.kind_of?(Response) # InternalError ends up here if no handlers [e.status, e.headers, e.body] else # fallback and see if there's any InternalError handler log_error(e, stderr) handle(ctrl, InternalError.new) end end def best_handler e handlers = self.class.handlers if handlers.key?(e.class) handlers[e.class] else # or find the nearest match and cache it ancestors = e.class.ancestors handlers[e.class] = handlers. inject([nil, Float::INFINITY]){ |(handler, val), (klass, block)| idx = ancestors.index(klass) || Float::INFINITY # lower is better if idx < val then [block, idx] else [handler, val] end }.first end end # ----------------------------------------------------------------- def self.included mod mod.__send__(:extend, DSL) mod.__send__(:attr_reader, :app) mod.handle_exceptions(true) end # ----------------------------------------------------------------- module_function def public_root "#{File.dirname(__FILE__)}/jellyfish/public" end end