diff --git a/lib/merb-core/dispatch/router.rb b/lib/merb-core/dispatch/router.rb index fe597865..d9aa1c3d 100644 --- a/lib/merb-core/dispatch/router.rb +++ b/lib/merb-core/dispatch/router.rb @@ -6,19 +6,19 @@ module Merb # Router stores route definitions and finds the first # route that matches the incoming request URL. - # + # # Then information from route is used by dispatcher to # call action on the controller. - # + # # ==== Routes compilation. - # + # # The most interesting method of Router (and heart of # route matching machinery) is match method generated # on the fly from routes definitions. It is called routes # compilation. Generated match method body contains # one if/elsif statement that picks the first matching route # definition and sets values to named parameters of the route. - # + # # Compilation is synchronized by mutex. class Router @routes = [] @@ -26,14 +26,14 @@ class Router @resource_routes = {} @compiler_mutex = Mutex.new @root_behavior = Behavior.new.defaults(:action => "index") - + # Raised when route lookup fails. class RouteNotFound < StandardError; end; # Raised when parameters given to generation # method do not match route parameters. class GenerationError < StandardError; end; class NotCompiledError < StandardError; end; - + class << self # @private attr_accessor :routes, :named_routes, :resource_routes, :root_behavior @@ -41,19 +41,20 @@ class << self # Creates a route building context and evaluates the block in it. A # copy of +root_behavior+ (and instance of Behavior) is copied as # the context. - # + # # ==== Parameters # first:: # An array containing routes that should be prepended to the routes # defined in the block. - # # last:: # An array containing routes that should be appended to the routes # defined in the block. - # + # # ==== Returns # Merb::Router:: # Returns self to allow chaining of methods. + # + # @api public def prepare(first = [], last = [], &block) @routes = [] root_behavior._with_proxy(&block) @@ -63,17 +64,23 @@ def prepare(first = [], last = [], &block) end # Appends route in the block to routing table. + # + # @api public def append(&block) prepare(routes, [], &block) end - + # Prepends routes in the block to routing table. + # + # @api public def prepend(&block) prepare([], routes, &block) end # Clears the routing table. Route generation and request matching # won't work anymore until a new routing table is built. + # + # @api private def reset! class << self alias_method :match, :match_before_compilation @@ -84,18 +91,16 @@ class << self # Finds route matching URI of the request and returns a tuple of # [route index, route params]. This method is called by the # dispatcher and isn't as useful in applications. - # + # # ==== Parameters # request:: request to match. - # + # # ==== Returns - # :: # The name of the route to generate - # + # # anonymous_params:: # An array of anonymous parameters to generate the route # with. These parameters are assigned to the route parameters # in the order that they are passed. - # + # # params:: # Named parameters to generate the route with. - # + # # defaults:: # A hash of default parameters to generate the route with. # This is usually the request parameters. If there are any # required params that are missing to generate the route, # they are pulled from this hash. + # + # ==== Example + # url(:edit_node, node.id, :foo => "bar") + # url(:edit_site_rating, site.id, rating.id, :foo => "bar") + # # ==== Returns # String:: The generated URL - # --- - # @private + # + # @api private def url(name, *args) unless name.is_a?(Symbol) args.unshift(name) name = :default end - + unless route = Merb::Router.named_routes[name] raise Merb::Router::GenerationError, "Named route not found: #{name}" end @@ -152,25 +173,25 @@ def url(name, *args) end # Generates a URL from the resource(s) - # + # # ==== Parameters # resources:: # The identifiers for the resource route to generate. These # can either be symbols or objects. Symbols denote resource # collection routes and objects denote the members. - # + # # params:: # Any extra parameters needed to generate the route. # ==== Returns # String:: The generated URL - # --- - # @private + # + # @api private def resource(*args) defaults = args.pop options = extract_options_from_args!(args) || {} key = [] params = [] - + args.each do |arg| if arg.is_a?(Symbol) || arg.is_a?(String) key << arg.to_s @@ -179,19 +200,21 @@ def resource(*args) params << arg end end - + params << options - + unless route = Merb::Router.resource_routes[key] raise Merb::Router::GenerationError, "Resource route not found: #{args.inspect}" end - + route.generate(params, defaults) end - - private - - # Defines method with a switch statement that does routes recognition. + + private + + # Compiles the routes and creates the +match+ method. + # + # @api private def compile if routes.any? eval(compiled_statement, binding, "Generated Code for Router", 1) @@ -199,18 +222,21 @@ def compile reset! end end - - # Generates method that does route recognition with a switch statement. + + # Generates the method for evaluation defining a +match+ method to match + # a request with the defined routes. + # + # @api private def compiled_statement @compiler_mutex.synchronize do condition_keys, if_statements = Set.new, "" - + routes.each_with_index do |route, i| route.freeze route.conditions.keys.each { |key| condition_keys << key } if_statements << route.compiled_statement(i == 0) end - + statement = "def match(request)\n" statement << condition_keys.inject("") do |cached, key| cached << " cached_#{key} = request.#{key}.to_s\n" @@ -222,7 +248,7 @@ def compiled_statement statement << "end" end end - + end # class << self end end diff --git a/lib/merb-core/dispatch/router/behavior.rb b/lib/merb-core/dispatch/router/behavior.rb index c51be1f4..6178b2a8 100644 --- a/lib/merb-core/dispatch/router/behavior.rb +++ b/lib/merb-core/dispatch/router/behavior.rb @@ -3,31 +3,51 @@ module Merb class Router class Behavior - - class Error < StandardError; end; + + class Error < StandardError; end # Proxy catches any methods and proxies them to the current behavior. # This allows building routes without constantly having to catching the # yielded behavior object - # --- - # @private - class Proxy #:nodoc: + # + # @api private + class Proxy + # Undefine as many methods as possible so that everything can be proxied # along to the behavior instance_methods.each { |m| undef_method m unless %w[ __id__ __send__ class kind_of? respond_to? assert_kind_of should should_not instance_variable_set instance_variable_get instance_eval].include?(m) } + # @api private def initialize @behaviors = [] end + # Puts a behavior on the bottom of the stack. + # + # ==== Notes + # The behaviors keep track of nested scopes. + # + # @api private def push(behavior) @behaviors.push(behavior) end + # Removes the top-most behavior. + # + # ==== Notes + # This occurs at the end of a nested scope (namespace, etc). + # + # @api private def pop @behaviors.pop end + # Tests whether the top-most behavior responds to the arguments. + # + # ==== Notes + # Behaviors contain the actual functionality of the proxy. + # + # @api private def respond_to?(*args) super || @behaviors.last.respond_to?(*args) end @@ -35,7 +55,7 @@ def respond_to?(*args) # Rake does some stuff with methods in the global namespace, so if I don't # explicitly define the Behavior methods to proxy here (specifically namespace) # Rake's methods take precedence. - # --- + # # Removing the following: # name full_name fixatable redirect %w( @@ -49,25 +69,38 @@ def #{method}(*args, &block) } end - # --- These methods are to be used in defer_to blocks + # == These methods are to be used in defer_to blocks - # Generates a URL from the passed arguments. This method is for use - # inside of defer_to + # Generates a URL from the passed arguments. + # + # ==== Notes + # This method is for use inside of defer_to. + # + # @api public def url(name, *args) args << {} Merb::Router.url(name, *args) end + # Generates a Rack redirection response. + # + # ==== Notes + # Refer to Merb::Rack::Helpers.redirect for documentation. + # + # @api public def redirect(url, opts = {}) Merb::Rack::Helpers.redirect(url, opts) end - def route(params) - params - end + private - private - + # Proxies the method calls to the behavior. + # + # ==== Notes + # Please refer to: + # http://ruby-doc.org/core/classes/Kernel.html#M005951 + # + # @api private def method_missing(method, *args, &block) behavior = @behaviors.last @@ -78,14 +111,14 @@ def method_missing(method, *args, &block) end end end - + # Behavior objects are used for the Route building DSL. Each object keeps # track of the current definitions for the level at which it is defined. # Each time a method is called on a Behavior object that accepts a block, # a new instance of the Behavior class is created. - # + # # ==== Parameters - # + # # proxy:: # This is the object initialized by Merb::Router.prepare that tracks the # current Behavior object stack so that Behavior methods can be called @@ -100,11 +133,11 @@ def method_missing(method, *args, &block) # The initial route options. See #options. # blocks:: # The stack of deferred routing blocks for the route - # + # # ==== Returns # Behavior:: The initialized Behavior object - #--- - # @private + # + # @api private def initialize(proxy = nil, conditions = {}, params = {}, defaults = {}, identifiers = {}, options = {}, blocks = []) #:nodoc: @proxy = proxy @conditions = conditions @@ -113,130 +146,129 @@ def initialize(proxy = nil, conditions = {}, params = {}, defaults = {}, identif @identifiers = identifiers @options = options @blocks = blocks - + stringify_condition_values end - + # Defines the +conditions+ that are required to match a Request. Each # +condition+ is applied to a method of the Request object. Conditions # can also be applied to segments of the +path+. - # + # # If #match is passed a block, it will create a new route scope with # the conditions passed to it and yield to the block such that all # routes that are defined in the block have the conditions applied # to them. - # + # # ==== Parameters - # + # # path:: # The pattern against which Merb::Request path is matched. - # + # # When +path+ is a String, any substring that is wrapped in parenthesis # is considered optional and any segment that begins with a colon, ex.: # ":login", defines both a capture and a named param. Extra conditions # can then be applied each named param individually. - # + # # When +path+ is a Regexp, the pattern is left untouched and the # Merb::Request path is matched against it as is. # # +path+ is optional. - # + # # conditions:: # Additional conditions that the request must meet in order to match. # The keys must be the names of previously defined path segments or # be methods that the Merb::Request instance will respond to. The # value is the string or regexp that matched the returned value. # Conditions are inherited by child routes. - # + # # &block:: # All routes defined in the block will be scoped to the conditions # defined by the #match method. - # + # # ==== Block parameters # r:: +optional+ - The match behavior object. - # + # # ==== Returns # Behavior:: # A new instance of Behavior with the specified path and conditions. - # + # # +Tip+: When nesting always make sure the most inner sub-match registers - # a Route and doesn't just returns new Behaviors. - # + # a Route and doesn't just return new Behaviors. + # # ==== Examples - # + # # # registers /foo/bar to controller => "foo", :action => "bar" # # and /foo/baz to controller => "foo", :action => "baz" # match("/foo") do # match("/bar").to(:controller => "foo", :action => "bar") # match("/baz").to(:controller => "foo", :action => "caz") # end - # + # # # Checks the format of the segments against the specified Regexp # match("/:string/:number", :string => /[a-z]+/, :number => /\d+/). # to(:controller => "string_or_numbers") - # + # # # Equivalent to the default_route # match("/:controller(/:action(:id))(.:format)").register - # + # # #match only if the browser string contains MSIE or Gecko # match("/foo", :user_agent => /(MSIE|Gecko)/ ) # .to(:controller => 'foo', :action => 'popular') - # + # # # Route GET and POST requests to different actions (see also #resources) # r.match('/foo', :method => :get).to(:action => 'show') # r.match('/foo', :method => :post).to(:action => 'create') - # + # # # match also takes regular expressions - # + # # r.match(%r[/account/([a-z]{4,6})]).to(:controller => "account", # :action => "show", :id => "[1]") - # + # # r.match(%r{/?(en|es|fr|be|nl)?}).to(:language => "[1]") do # match("/guides/:action/:id").to(:controller => "tour_guides") # end - #--- - # @public + # + # @api public def match(path = {}, conditions = {}, &block) path, conditions = path[:path], path if path.is_a?(Hash) - - raise Error, "The route has already been committed. Further conditions cannot be specified" if @route + raise Error, "The route has already been committed. Further conditions cannot be specified" if @route conditions.delete_if { |k, v| v.nil? } conditions[:path] = merge_paths(path) - + behavior = Behavior.new(@proxy, @conditions.merge(conditions), @params, @defaults, @identifiers, @options, @blocks) with_behavior_context(behavior, &block) end # Creates a Route from one or more Behavior objects, unless a +block+ is # passed in. - # + # # ==== Parameters # params:: The parameters the route maps to. - # + # # &block:: # All routes defined in the block will be scoped to the params # defined by the #to method. - # + # # ==== Block parameters # r:: +optional+ - The to behavior object. - # + # # ==== Returns # Route:: It registers a new route and returns it. - # + # # ==== Examples # match('/:controller/:id).to(:action => 'show') - # + # # to(:controller => 'simple') do # match('/test').to(:action => 'index') # match('/other').to(:action => 'other') # end - #--- - # @public + # + # @api public def to(params = {}, &block) raise Error, "The route has already been committed. Further params cannot be specified" if @route - + behavior = Behavior.new(@proxy, @conditions, @params.merge(params), @defaults, @identifiers, @options, @blocks) if block_given? @@ -247,7 +279,8 @@ def to(params = {}, &block) end # Equivalent of #to. Allows for some nicer syntax when scoping blocks - # --- Ex: + # + # ==== Examples # Merb::Router.prepare do # with(:controller => "users") do # match("/signup").to(:action => "signup") @@ -255,32 +288,32 @@ def to(params = {}, &block) # match("/logout").to(:action => "logout") # end # end - alias_method :with, :to + alias :with :to # Equivalent of #to. Allows for nicer syntax when registering routes with no params - # --- Ex: + # + # ==== Examples # Merb::Router.prepare do # match("/:controller(/:action(/:id))(.:format)").register # end - # - alias_method :register, :to + alias :register :to # Sets default values for route parameters. If no value for the key # can be extracted from the request, then the value provided here # will be used. - # + # # ==== Parameters # defaults:: # The default values for named segments. - # + # # &block:: # All routes defined in the block will be scoped to the defaults defined # by the #default method. - # + # # ==== Block parameters # r:: +optional+ - The defaults behavior object. - # --- - # @public + # + # @api public def default(defaults = {}, &block) behavior = Behavior.new(@proxy, @conditions, @params, @defaults.merge(defaults), @identifiers, @options, @blocks) with_behavior_context(behavior, &block) @@ -289,34 +322,34 @@ def default(defaults = {}, &block) alias_method :defaults, :default # Allows the fine tuning of certain router options. - # + # # ==== Parameters # options:: # The options to set for all routes defined in the scope. The currently # supported options are: # * :controller_prefix - The module that the controller is included in. # * :name_prefix - The prefix added to all routes named with #name - # + # # &block:: # All routes defined in the block will be scoped to the options defined # by the #options method. - # + # # ==== Block parameters # r:: The options behavior object. This is optional - # + # # ==== Examples # # If :group is not matched in the path, it will be "registered" instead # # of nil. # match("/users(/:group)").default(:group => "registered") - # --- - # @public + # + # @api public def options(opts = {}, &block) options = @options.dup - + opts.each_pair do |key, value| options[key] = (options[key] || []) + [value.freeze] if value end - + behavior = Behavior.new(@proxy, @conditions, @params, @defaults, @identifiers, options, @blocks) with_behavior_context(behavior, &block) end @@ -325,44 +358,47 @@ def options(opts = {}, &block) # Creates a namespace for a route. This way you can have logical # separation to your routes. - # + # # ==== Parameters # name_or_path:: # The name or path of the namespace. - # + # # options:: - # Optional hash, set :path if you want to override what appears on the url - # + # Optional hash (see below) + # # &block:: # All routes defined in the block will be scoped to the namespace defined # by the #namespace method. - # + # + # ==== Options (opts) + # :path:: match against this url + # # ==== Block parameters # r:: The namespace behavior object. This is optional - # + # # ==== Examples # namespace :admin do # resources :accounts # resource :email # end - # + # # # /super_admin/accounts # namespace(:admin, :path=>"super_admin") do # resources :accounts # end - # --- - # @public + # + # @api public def namespace(name_or_path, opts = {}, &block) name = name_or_path.to_s # We don't want this modified ever path = opts.has_key?(:path) ? opts[:path] : name - + raise Error, "The route has already been committed. Further options cannot be specified" if @route - + # option keys could be nil opts[:controller_prefix] = name unless opts.has_key?(:controller_prefix) opts[:name_prefix] = name unless opts.has_key?(:name_prefix) opts[:resource_prefix] = opts[:name_prefix] unless opts.has_key?(:resource_prefix) - + behavior = self behavior = behavior.match("/#{path}") unless path.nil? || path.empty? behavior.options(opts, &block) @@ -372,22 +408,22 @@ def namespace(name_or_path, opts = {}, &block) # insertion into a route. This is useful when using models and want a # specific method to be called on it (For example, for ActiveRecord::Base # it would be #to_param). - # + # # The default method called on objects is #to_s. - # + # # ==== Paramters # identifiers:: # The keys are Classes and the values are the method that instances of the specified # class should have called on. - # + # # &block:: # All routes defined in the block will be call the specified methods during # generation. - # + # # ==== Block parameters # r:: The identify behavior object. This is optional - # --- - # @public + # + # @api public def identify(identifiers = {}, &block) identifiers = if Hash === identifiers @identifiers.merge(identifiers) @@ -402,23 +438,23 @@ def identify(identifiers = {}, &block) # Creates the most common routes /:controller/:action/:id.format when # called with no arguments. You can pass a hash or a block to add parameters # or override the default behavior. - # + # # ==== Parameters # params:: # This optional hash can be used to augment the default settings - # + # # &block:: # When passing a block a new behavior is yielded and more refinement is # possible. - # + # # ==== Returns # Route:: the default route - # + # # ==== Examples - # + # # # Passing an extra parameter "mode" to all matches # r.default_routes :mode => "default" - # + # # # specifying exceptions within a block # r.default_routes do |nr| # nr.defer_to do |request, params| @@ -426,31 +462,31 @@ def identify(identifiers = {}, &block) # :action => "new") if request.env["REQUEST_URI"] =~ /\/private\// # end # end - #--- - # @public + # + # @api public def default_routes(params = {}, &block) match("/:controller(/:action(/:id))(.:format)").to(params, &block).name(:default) end # Takes a block and stores it for deferred conditional routes. The block # takes the +request+ object and the +params+ hash as parameters. - # + # # ==== Parameters # params:: Parameters and conditions associated with this behavior. # &conditional_block:: # A block with the conditions to be met for the behavior to take # effect. - # + # # ==== Returns # Route :: The default route. - # + # # ==== Examples # defer_to do |request, params| # params.merge :controller => 'here', # :action => 'there' if request.xhr? # end - #--- - # @public + # + # @api public def defer_to(params = {}, &block) defer(block).to(params) end @@ -458,27 +494,39 @@ def defer_to(params = {}, &block) # Takes a Proc as a parameter and applies it as a deferred proc for all the # routes defined in the block. This is mostly interesting for plugin # developers. + # + # ==== Examples + # defered_block = proc do |r, p| + # p.merge :controller => 'api/comments' if request.xhr? + # end + # defer(defered_block) do + # resources :comments + # end + # + # @api public def defer(deferred_block, &block) blocks = @blocks + [CachedProc.new(deferred_block)] behavior = Behavior.new(@proxy, @conditions, @params, @defaults, @identifiers, @options, blocks) with_behavior_context(behavior, &block) end - # Names this route in Router. Name must be a Symbol. - # + # Registers the route as a named route with the name given. + # # ==== Parameters - # symbol:: The name of the route. - # + # symbol:: the name of the route. + # # ==== Raises # ArgumentError:: symbol is not a Symbol. + # + # @api public def name(prefix, name = nil) unless name name, prefix = prefix, nil end - + full_name([prefix, @options[:name_prefix], name].flatten.compact.join('_')) end - + # Names this route in Router. Name must be a Symbol. The current # name_prefix is ignored. # @@ -487,6 +535,8 @@ def name(prefix, name = nil) # # ==== Raises # ArgumentError:: symbol is not a Symbol. + # + # @api private def full_name(name) if @route @route.name = name @@ -496,27 +546,34 @@ def full_name(name) end end + # Specifies that a route can be fixatable. + # # ==== Parameters # enabled:: True enables fixation on the route. + # + # @api public def fixatable(enable = true) @route.fixation = enable self end - - # Sets the current route as a redirect. - # + + # Redirects the current route. + # # ==== Parameters - # path:: The path to redirect to. + # # options:: - # Options for the redirect - # The supported options are: - # * :permanent: Whether or not the redirect should be permanent. - # The default value is false. + # Options (see below) + # + # ==== Options (opts) + # :permanent:: + # Whether or not the redirect should be permanent. + # The default value is false. + # + # @api public def redirect(url, opts = {}) raise Error, "The route has already been committed." if @route - + status = opts[:permanent] ? 301 : 302 @route = Route.new(@conditions, {:url => url.freeze, :status => status.freeze}, @blocks, :redirects => true) @route.register @@ -529,6 +586,8 @@ def redirect(url, opts = {}) # it doesn't affect how/which routes are added. # # &block:: A context in which routes are generated. + # + # @api public def capture(&block) captured_routes = {} name_prefix = [@options[:name_prefix]].flatten.compact.map { |p| "#{p}_"} @@ -545,27 +604,42 @@ def capture(&block) captured_routes end - # So that Router can have a default route - # --- - # @private - def _with_proxy(&block) #:nodoc: + # Proxy routes with the default behaviors. + # + # ==== Parameters + # &block:: defines routes within the provided context. + # + # @api private + def _with_proxy(&block) proxy = Proxy.new proxy.push Behavior.new(proxy, @conditions, @params, @defaults, @identifiers, @options, @blocks) proxy.instance_eval(&block) proxy end - protected - + protected + + # Returns the current route. + # + # ==== Returns + # Route:: the route. + # + # @api private def _route @route end - def to_route # :nodoc: + # Turns a route definition into a Route object. + # + # ==== Returns + # Route:: the route generated. + # + # @api private + def to_route raise Error, "The route has already been committed." if @route - + controller = @params[:controller] - + if prefixes = @options[:controller_prefix] controller ||= ":controller" @@ -594,12 +668,22 @@ def to_route # :nodoc: # Allows to insert the route at a certain spot in the list of routes # instead of appending to the list. - def before(route, &block) #:nodoc: + # + # ==== Params + # route:: the route to insert before. + # &block:: the route definition to insert. + # + # @api plugin + def before(route, &block) options(:before => route, &block) end - - private - + + private + + # Takes @conditions and turns values into strings (except for Regexp and + # Array values). + # + # @api private def stringify_condition_values # :nodoc: @conditions.each do |key, value| unless value.nil? || Regexp === value || Array === value @@ -607,7 +691,18 @@ def stringify_condition_values # :nodoc: end end end - + + # Creates a new context with a given behavior for the route definition in + # the block. + # + # ==== Params + # behavior:: the behavior to proxy the calls in the block. + # &block:: the routing definitions to be nested within the behavior. + # + # ==== Returns + # Behavior:: the behavior wrapping. + # + # @api private def with_behavior_context(behavior, &block) # :nodoc: if block_given? @proxy.push(behavior) @@ -616,11 +711,25 @@ def with_behavior_context(behavior, &block) # :nodoc: end behavior end - + + # Merges the path elements together into an array to be joined for + # generating named routes. + # + # ==== Parameters + # path:: the path to merge at the end. + # + # ==== Returns + # Array:: array of path elements. + # + # ==== Notes + # An array of ['a', 'b'] (the 'a' namespace with the 'b' action) will + # produce a name of :a_b. + # + # @api private def merge_paths(path) # :nodoc: [@conditions[:path], path.freeze].flatten.compact end - + end end end \ No newline at end of file