# This file is part of the "Utopia Framework" project, and is licensed under the GNU AGPLv3. # Copyright 2010 Samuel Williams. All rights reserved. # See for licensing details. require 'utopia/middleware' require 'utopia/path' require 'utopia/http_status_codes' class Rack::Request def controller(&block) if block_given? env["utopia.controller"].instance_eval(&block) else env["utopia.controller"] end end end module Utopia module Middleware class Controller CONTROLLER_RB = "controller.rb" class Variables def [](key) instance_variable_get("@#{key}") end def []=(key, value) instance_variable_set("@#{key}", value) end end class Base def initialize(controller) @controller = controller @actions = {} methods.each do |method_name| next unless method_name.match(/on_(.*)$/) action($1.split("_")) do |path, request| # LOG.debug("Controller: #{method_name}") self.send(method_name, path, request) end end end def action(path, options = {}, &block) cur = @actions path.reverse.each do |name| cur = cur[name] ||= {} end cur[:action] = Proc.new(&block) end def lookup(path) cur = @actions path.components.reverse.each do |name| cur = cur[name] return nil if cur == nil if action = cur[:action] return action end end end # Given a request, call an associated action if one exists def passthrough(path, request) action = lookup(path) if action return respond_with(action.call(path, request)) end return nil end def call(env) @controller.app.call(env) end def redirect(target, status = 302) {:redirect => target, :status => status} end def respond_with(*args) return args[0] if args[0] == nil || Array === args[0] status = 200 options = nil if Numeric === args[0] || Symbol === args[0] status = args[0] options = args[1] || {} else options = args[0] status = options[:status] || status end status = Utopia::HTTP_STATUS_CODES[status] || status headers = options[:headers] || {} if options[:type] headers['Content-Type'] ||= options[:type] end body = [] if options[:body] body = options[:body] elsif options[:content] body = [options[:content]] elsif status >= 300 body = [Utopia::HTTP_STATUS_DESCRIPTIONS[status] || 'Status #{status}'] end if options[:redirect] headers["Location"] = options[:redirect] status = 302 if status < 300 || status >= 400 end # Utopia::LOG.debug([status, headers, body].inspect) return [status, headers, body] end def process!(path, request) passthrough(path, request) end def self.require_local(path) require(File.join(const_get('BASE_PATH'), path)) end end def initialize(app, options = {}) @app = app @root = options[:root] || Utopia::Middleware::default_root LOG.info "#{self.class.name}: Running in #{@root}" @controllers = {} @cache_controllers = (UTOPIA_ENV == :production) if options[:controller_file] @controller_file = options[:controller_file] else @controller_file = "controller.rb" end end attr :app def lookup(path) if @cache_controllers return @controllers.fetch(path.to_s) do |key| @controllers[key] = load_file(path) end else return load_file(path) end end def load_file(path) if path.directory? base_path = File.join(@root, path.components) else base_path = File.join(@root, path.dirname.components) end controller_path = File.join(base_path, CONTROLLER_RB) if File.exist?(controller_path) klass = Class.new(Base) klass.const_set('BASE_PATH', base_path) $LOAD_PATH.unshift(base_path) klass.class_eval(File.read(controller_path), controller_path) $LOAD_PATH.delete(base_path) return klass.new(self) else return nil end end def fetch_controllers(path) controllers = [] path.ascend do |parent_path| controllers << lookup(parent_path) end return controllers.compact.reverse end def call(env) env["utopia.controller"] ||= Variables.new request = Rack::Request.new(env) path = Path.create(request.path_info) fetch_controllers(path).each do |controller| if result = controller.process!(path, request) return result end end return @app.call(env) end end end end