require 'merb-core/dispatch/router/cached_proc'
require 'merb-core/dispatch/router/behavior'
require 'merb-core/dispatch/router/route'
require 'merb-core/controller/mixins/responder'
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
SEGMENT_REGEXP = /(:([a-z_][a-z0-9_]*|:))/
SEGMENT_REGEXP_WITH_BRACKETS = /(:[a-z_]+)(\[(\d+)\])?/
JUST_BRACKETS = /\[(\d+)\]/
PARENTHETICAL_SEGMENT_STRING = "([^\/.,;?]+)".freeze
@@named_routes = {}
@@routes = []
@@compiler_mutex = Mutex.new
cattr_accessor :routes, :named_routes
class << self
# Finds route matching URI of the request and
# returns a tuple of [route index, route params].
#
# ==== Parameters
# request<Merb::Request>:: request to match.
#
# ==== Returns
# <Array(Integer, Hash)::
# Two-tuple: route index and route parameters. Route
# parameters are :controller, :action and all the named
# segments of the route.
def route_for(request)
index, params = match(request)
route = routes[index] if index
if !route
raise ControllerExceptions::NotFound,
"No routes match the request: #{request.uri}"
end
[route, params]
end
# Clear all routes.
def reset!
self.routes, self.named_routes = [], {}
end
# Appends the generated routes to the current routes.
#
# ==== Parameters
# &block::
# A block that generates new routes when yielded a new Behavior.
def append(&block)
prepare(@@routes, [], &block)
end
# Prepends the generated routes to the current routes.
#
# ==== Parameters
# &block::
# A block that generates new routes when yielded a new Behavior.
def prepend(&block)
prepare([], @@routes, &block)
end
# Prepares new routes and adds them to existing routes.
#
# ==== Parameters
# first<Array>:: An array of routes to add before the generated routes.
# last<Array>:: An array of routes to add after the generated routes.
# &block:: A block that generates new routes.
#
# ==== Block parameters (&block)
# new_behavior<Behavior>:: Behavior for child routes.
def prepare(first = [], last = [], &block)
@@routes = []
yield Behavior.new({}, { :action => 'index' }) # defaults
@@routes = first + @@routes + last
compile
end
# Capture any new routes that have been added within the block.
#
# This utility method lets you track routes that have been added;
# it doesn't affect how/which routes are added.
#
# &block:: A context in which routes are generated.
def capture(&block)
routes_before, named_route_keys_before = self.routes.dup, self.named_routes.keys
yield
[self.routes - routes_before, self.named_routes.except(*named_route_keys_before)]
end
# ==== Returns
# String:: A routing lambda statement generated from the routes.
def compiled_statement
@@compiler_mutex.synchronize do
@@compiled_statement = "def match(request)\n"
@@compiled_statement << " params = request.params\n"
@@compiled_statement << " cached_path = request.path\n cached_method = request.method.to_s\n "
@@routes.each_with_index { |route, i| @@compiled_statement << route.compile(i == 0) }
@@compiled_statement << " else\n [nil, {}]\n"
@@compiled_statement << " end\n"
@@compiled_statement << "end"
end
end
# Defines the match function for this class based on the
# compiled_statement.
def compile
puts "compiled route: #{compiled_statement}" if $DEBUG
eval(compiled_statement, binding, "Generated Code for Router#match(#{__FILE__}:#{__LINE__})", 1)
end
# Generates a URL based on passed options.
#
# ==== Parameters
# name<~to_sym, Hash>:: The name of the route to generate.
# params<Hash, Fixnum, Object>:: The params to use in the route generation.
# fallback<Hash>:: Parameters for generating a fallback URL.
#
# ==== Returns
# String:: The generated URL.
#
# ==== Alternatives
# If name is a hash, it will be merged with params and passed on to
# generate_for_default_route along with fallback.
def generate(name, params = {}, fallback = {})
params.reject! { |k,v| v.nil? } if params.is_a? Hash
if name.is_a? Hash
name.reject! { |k,v| v.nil? }
return generate_for_default_route(name.merge(params), fallback)
end
name = name.to_sym
unless @@named_routes.key? name
raise "Named route not found: #{name}"
else
@@named_routes[name].generate(params, fallback)
end
end
# Generates a URL based on the default route scheme of
# "/:controller/:action/:id.:format".
#
# ==== Parameters
# params<Hash>::
# The primary parameters to create the route from (see below).
# fallback<Hash>:: Fallback parameters. Same options as params.
#
# ==== Options (params)
# :controller<~to_s>:: The controller name. Required.
# :action<~to_s>:: The action name. Required.
# :id<~to_s>:: The ID for use in the action.
# :format<~to_s>:: The format of the preferred response.
#
# ==== Returns
# String:: The generated URL.
def generate_for_default_route(params, fallback)
query_params = params.reject do |k,v|
[:controller, :action, :id, :format, :fragment].include?(k.to_sym)
end
controller = params[:controller] || fallback[:controller]
raise "Controller Not Specified" unless controller
url = "/#{controller}"
if params[:action] || params[:id] || params[:format] || !query_params.empty?
action = params[:action] || fallback[:action]
raise "Action Not Specified" unless action
url += "/#{action}"
end
if params[:id]
url += "/#{params[:id]}"
end
if format = params[:format]
format = fallback[:format] if format == :current
url += ".#{format}"
end
unless query_params.empty?
url += "?" + Merb::Request.params_to_query_string(query_params)
end
if params[:fragment]
url += "##{params[:fragment]}"
end
url
end
end # self
end
end