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 ActionError < 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 server. attr_accessor :server # 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) # Call the mounted callback to allow for post mount # initialization. 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) return # FIXME: make the following work again! 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_rule(:match => route.first, :controller => c, :action => m, :params => route.last) end end end end # Processes the path and dispatches to the corresponding # controller/action pair. # # [+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. # The dispatcher also handles nested controllers. # # Returns the dispatcher class and the action name. #-- # 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 # This adds parameter values from the setup from the route to the normal # query strings. context.headers['QUERY_STRING'] ||= '' extra = params.map { |k, v| "#{k}=#{v}"}.join(';') if params if context.headers['QUERY_STRING'].empty? context.headers['QUERY_STRING'] = extra else context.headers['QUERY_STRING'] << ';' << extra end context.headers['ACTION_PARAMS'] = params.values # context.params.update(params) if params # gmosx, FIXME/OPTIMIZE: no annotation for mount point!! return klass, "#{action}_action", klass.mount_path end key, * = path.split('?', 2) key = key.split('/') parts = [] while (not key.empty?) and (klass = controller_class_for("#{key.join('/')}")).nil? parts.unshift(key.pop) end klass = controller_class_for(ROOT) unless klass idx = 0 found = false # gmosx, HACKFIX! parts.shift if parts.first == '' # 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 # Check the index action. unless found action = :index if klass.respond_to_action? action a = klass.instance_method(action).arity found = true if a < 0 || a >= parts.size elsif klass.respond_to_template? action found = true if parts.size == 0 end idx = -1 if found end if found parts.slice!(0, idx + 1) =begin if $DBG # Some extra checking of the parameters. Only enabled # on debug mode, because it slows down dispatching. a = klass.instance_method(action).arity if a > 0 and a != parts.size raise ActionError, "Invalid parameters for action, expects #{a} parameters, received #{parts.size}" end end =end else #-- # FIXME: no raise to make testable. #++ raise ActionError, "No action for path '#{path}' on '#{klass}'" end # push any remaining parts of the url onto the query # string for use with request context.headers['ACTION_PARAMS'] = parts return klass, "#{action}_action" 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