require 'nitro/controller' require 'nitro/compiler' require 'nitro/router' 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, klass in controllers unless (klass.ancestors.include?(Controller) or klass.ancestors.include?(Publishable)) klass.send :include, Publishable end # Automatically mixin controller helpers. mixin_auto_helpers(klass) # Customize the class for mounting at the given path. #-- # gmosx, TODO: path should include trailing '/' # gmosx, TODO: should actually create an instance, thus # allowing mounting the same controller to multiple # paths, plus simplifying the code. This instance will # be dup-ed for each request. #++ klass.mount_at(path) klass.mounted(path) if klass.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 mixin_auto_helpers(klass) klass.helper(Nitro::DefaultHelper) begin if helper = Module.by_name("#{klass}Helper") klass.helper(helper) end rescue NameError # The auto helper is not defined. end end # Update the routes. Typically called after a new # Controller is mounted. # # === Example of routing through annotations # # def view_user # "params: #{request[:id]} and #{request[:mode]}" # end # ann :view_user, :route => [ /user_(\d*)_(*?)\.html/, :id, :mode ] def update_routes init_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?) add_route(route.first, :controller => c, :action => m, :params => route.last) 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) # Try if the router can directly decode the path. klass, action, params = decode_route(path) if klass context.params.update(params) if params # gmosx, FIXME/OPTIMIZE: no annotation for mount point!! return klass, "#{action}_action", klass.mount_path end 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