require 'nano/kernel/singleton' require 'nitro/controller' require 'nitro/compiler' require 'nitro/routing' require 'nitro/helper/default' module Nitro # Raised when an action can not be found for a path # check for this in your error action to catch as if 404 class NoActionError < NoMethodError; end # The Dispatcher manages a set of controllers. It maps # a request uri to a [controller, action] pair. class Dispatcher include Router ROOT = '/' # The controllers map. attr_accessor :controllers # Create a new Dispatcher. # # Input: # # [+controllers+] # Either a hash of controller mappings or a single # controller that gets mapped to :root. def initialize(controllers = nil) if controllers and controllers.is_a?(Class) and controllers.ancestors.include?(Controller) controllers = { '/' => controllers } else controllers ||= { '/' => Controller } end mount(controllers) end # A published object is exposed through a REST interface. # Only the public non standard methods of the object are # accessible. Published objects implement the Controller # part of MVC. # # Process the given hash and mount the # defined classes (controllers). # # Input: # # [+controllers+] # A hash representing the mapping of # mount points to controllers. # # === Examples # # disp.mount( # '/' => MainController, # mounts / # '/users' => UsersController # mounts /users # ) # disp.publish '/' => MainController def add_controller(controllers) for path, c in controllers unless (c.ancestors.include?(Controller) or c.ancestors.include?(Publishable)) c.send :include, Publishable end auto_mixin(c) # Try to setup a template_root if none is defined: unless c.template_root c.module_eval %{ def self.template_root "#{Template.root}#{path}".gsub(/\\/$/, '') end } end # Keep the mount point as an annotation. c.ann.self.mount_point = path.gsub(/^\//, '') c.mounted(path) if c.respond_to?(:mounted) end (@controllers ||= {}).update(controllers) update_routes() end alias_method :mount, :add_controller alias_method :publish, :add_controller alias_method :map=, :add_controller # Call this method to automatically include helpers in the # Controllers. For each Controller 'XxxController' the # default helper 'Helper' and the auto helper # 'XxxControllerHelper' (if it exists) are included. def auto_mixin(c) c.helper(Nitro::DefaultHelper) begin if helper = Module.by_name("#{c}Helper") c.helper(helper) end rescue NameError # The auto helper is not defined. end end # Update the routes. Typically called after a new # Controller is mounted. def update_routes @routes = [] @controllers.each do |base, c| base = '' if base == '/' for m in c.action_methods m = m.to_sym if route = c.ann(m).route and (!route.nil?) unless c.ann(m).params.nil? keys = c.ann(m).params.keys else keys = [] end @routes << [route, "#{base}/#{m}", *keys] end end end end # Processes the path and dispatches to the corresponding # controller/action pair. # The base returned contains a trailing '/'. # # [+path+] # The path to dispatch. # # [:context] # The dispatching context. # # The dispatching algorithm handles implicit nice urls. # Subdirectories are also supported. # Action containing '/' separators look for templates # in subdirectories. The '/' char is converted to '__' # to find the actual action. # # Returns the dispatcher class, the action name and the # base url. For the root path, the base url is nil. #-- # FIXME: this is a critical method that should be optimized # watch out for excessive String creation. # TODO: add caching. #++ def dispatch(path, context = nil) path = route(path, context) parts = path.split('/') parts.shift # get rid of the leading '/'. if klass = controller_class_for("/#{parts.first}") base = "/#{parts.shift}" else base = nil klass = controller_class_for(ROOT) end idx = 0 found = false # default to index parts << 'index' if parts.empty? # Try to find the first valid action substring action = '' for part in parts action << part if klass.respond_to_action_or_template?(action) found = true break end action << '__' idx += 1 end if found parts.slice!(0, idx + 1) else #-- # FIXME: no raise to make testable. #++ raise NoActionError, "No action for path '#{path}' on '#{klass}'" end # push any remaining parts of the url onto the query # string for use with request unless parts.empty? context.headers['QUERY_STRING'] = "#{parts.join(';')};#{context.headers['QUERY_STRING']}" end base = nil if base == ROOT return klass, "#{action}_action", base end alias_method :split_path, :dispatch private # Get the controller for the given key. # Also handles reloading of controllers. def controller_class_for(key) klass = @controllers[key] if $autoreload_dirty and klass and Compiler.reload Logger.info "Reloading controller '#{klass}'" klass.instance_methods.grep(/(_action$)|(_template$)/).each do |m| klass.send(:remove_method, m) rescue nil end klass.compile_scaffolding_code if klass.respond_to?(:compile_scaffolding_code) end return klass end end end # * George Moschovitis # * Chris Farmiloe