aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/routing
diff options
context:
space:
mode:
authorPratik Naik <pratiknaik@gmail.com>2010-01-04 03:24:39 +0530
committerPratik Naik <pratiknaik@gmail.com>2010-01-04 03:24:39 +0530
commitcda36a0731f14b33a920bf7e32255661e06f890a (patch)
tree79ccba37953f9fe3055503be42b1610faa6d64ad /actionpack/lib/action_dispatch/routing
parentbd4a3cce4ecd8e648179a91e26506e3622ac2162 (diff)
parenta115b5d79a850bb56cd3c9db9a05d6da35e3d7be (diff)
downloadrails-cda36a0731f14b33a920bf7e32255661e06f890a.tar.gz
rails-cda36a0731f14b33a920bf7e32255661e06f890a.tar.bz2
rails-cda36a0731f14b33a920bf7e32255661e06f890a.zip
Merge remote branch 'mainstream/master'
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r--actionpack/lib/action_dispatch/routing/deprecated_mapper.rb5
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb624
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb260
3 files changed, 517 insertions, 372 deletions
diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb
index 0564ba9797..8ce6b2f6d5 100644
--- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb
@@ -113,8 +113,7 @@ module ActionDispatch
end
end
- possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) }
- requirements[:controller] ||= Regexp.union(*possible_names)
+ requirements[:controller] ||= @set.controller_constraints
if defaults[:controller]
defaults[:action] ||= 'index'
@@ -176,7 +175,7 @@ module ActionDispatch
optional = false
elsif segment =~ /^:(\w+)$/
if defaults.has_key?($1.to_sym)
- defaults.delete($1.to_sym)
+ defaults.delete($1.to_sym) if defaults[$1.to_sym].nil?
else
optional = false
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 7d770dedd0..8f33346a4f 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,143 +1,261 @@
module ActionDispatch
module Routing
class Mapper
- module Resources
- def resource(*resources, &block)
- options = resources.last.is_a?(Hash) ? resources.pop : {}
-
- if resources.length > 1
- raise ArgumentError if block_given?
- resources.each { |r| resource(r, options) }
- return self
+ class Constraints
+ def self.new(app, constraints = [])
+ if constraints.any?
+ super(app, constraints)
+ else
+ app
end
+ end
- resource = resources.pop
+ def initialize(app, constraints = [])
+ @app, @constraints = app, constraints
+ end
- if @scope[:scope_level] == :resources
- member do
- resource(resource, options, &block)
+ def call(env)
+ req = Rack::Request.new(env)
+
+ @constraints.each { |constraint|
+ if constraint.respond_to?(:matches?) && !constraint.matches?(req)
+ return [ 404, {'X-Cascade' => 'pass'}, [] ]
+ elsif constraint.respond_to?(:call) && !constraint.call(req)
+ return [ 404, {'X-Cascade' => 'pass'}, [] ]
end
- return self
- end
+ }
- singular = resource.to_s
- plural = singular.pluralize
+ @app.call(env)
+ end
+ end
- controller(plural) do
- namespace(resource) do
- with_scope_level(:resource) do
- yield if block_given?
+ class Mapping
+ def initialize(set, scope, args)
+ @set, @scope = set, scope
+ @path, @options = extract_path_and_options(args)
+ end
- get "", :to => :show, :as => "#{singular}"
- post "", :to => :create
- put "", :to => :update
- delete "", :to => :destroy
- get "new", :to => :new, :as => "new_#{singular}"
- get "edit", :to => :edit, :as => "edit_#{singular}"
- end
+ def to_route
+ [ app, conditions, requirements, defaults, @options[:as] ]
+ end
+
+ private
+ def extract_path_and_options(args)
+ options = args.extract_options!
+
+ case
+ when using_to_shorthand?(args, options)
+ path, to = options.find { |name, value| name.is_a?(String) }
+ options.merge!(:to => to).delete(path) if path
+ when using_match_shorthand?(args, options)
+ path = args.first
+ options = { :to => path.gsub("/", "#"), :as => path.gsub("/", "_") }
+ else
+ path = args.first
end
+
+ [ normalize_path(path), options ]
end
- self
- end
+ # match "account" => "account#index"
+ def using_to_shorthand?(args, options)
+ args.empty? && options.present?
+ end
- def resources(*resources, &block)
- options = resources.last.is_a?(Hash) ? resources.pop : {}
+ # match "account/overview"
+ def using_match_shorthand?(args, options)
+ args.present? && options.except(:via).empty? && !args.first.include?(':')
+ end
- if resources.length > 1
- raise ArgumentError if block_given?
- resources.each { |r| resources(r, options) }
- return self
+ def normalize_path(path)
+ path = nil if path == ""
+ path = "#{@scope[:path]}#{path}" if @scope[:path]
+ path = Rack::Mount::Utils.normalize_path(path) if path
+
+ raise ArgumentError, "path is required" unless path
+
+ path
end
- resource = resources.pop
- if @scope[:scope_level] == :resources
- member do
- resources(resource, options, &block)
- end
- return self
+ def app
+ Constraints.new(
+ to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
+ blocks
+ )
end
- plural = resource.to_s
- singular = plural.singularize
+ def conditions
+ { :path_info => @path }.merge(constraints).merge(request_method_condition)
+ end
- controller(resource) do
- namespace(resource) do
- with_scope_level(:resources) do
- yield if block_given?
+ def requirements
+ @requirements ||= returning(@options[:constraints] || {}) do |requirements|
+ requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
+ @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
+ requirements[:controller] ||= @set.controller_constraints
+ end
+ end
- member do
- get "", :to => :show, :as => "#{singular}"
- put "", :to => :update
- delete "", :to => :destroy
- get "edit", :to => :edit, :as => "edit_#{singular}"
- end
+ def defaults
+ @defaults ||= if to.respond_to?(:call)
+ { }
+ else
+ defaults = case to
+ when String
+ controller, action = to.split('#')
+ { :controller => controller, :action => action }
+ when Symbol
+ { :action => to.to_s }.merge(default_controller ? { :controller => default_controller } : {})
+ else
+ default_controller ? { :controller => default_controller } : {}
+ end
- collection do
- get "", :to => :index, :as => "#{plural}"
- post "", :to => :create
- get "new", :to => :new, :as => "new_#{singular}"
- end
+ if defaults[:controller].blank? && segment_keys.exclude?("controller")
+ raise ArgumentError, "missing :controller"
end
+
+ if defaults[:action].blank? && segment_keys.exclude?("action")
+ raise ArgumentError, "missing :action"
+ end
+
+ defaults
end
end
- self
- end
- def collection
- unless @scope[:scope_level] == :resources
- raise ArgumentError, "can't use collection outside resources scope"
- end
+ def blocks
+ if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
+ block = @options[:constraints]
+ else
+ block = nil
+ end
- with_scope_level(:collection) do
- yield
+ ((@scope[:blocks] || []) + [ block ]).compact
end
- end
- def member
- unless @scope[:scope_level] == :resources
- raise ArgumentError, "can't use member outside resources scope"
+ def constraints
+ @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
end
- with_scope_level(:member) do
- scope(":id") do
- yield
+ def request_method_condition
+ if via = @options[:via]
+ via = Array(via).map { |m| m.to_s.upcase }
+ { :request_method => Regexp.union(*via) }
+ else
+ { }
end
end
+
+ def segment_keys
+ @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new(
+ Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS)
+ ).names
+ end
+
+ def to
+ @options[:to]
+ end
+
+ def default_controller
+ @scope[:controller].to_s if @scope[:controller]
+ end
+ end
+
+ module Base
+ def initialize(set)
+ @set = set
+ end
+
+ def root(options = {})
+ match '/', options.reverse_merge(:as => :root)
end
def match(*args)
+ @set.add_route(*Mapping.new(@set, @scope, args).to_route)
+ self
+ end
+ end
+
+ module HttpHelpers
+ def get(*args, &block)
+ map_method(:get, *args, &block)
+ end
+
+ def post(*args, &block)
+ map_method(:post, *args, &block)
+ end
+
+ def put(*args, &block)
+ map_method(:put, *args, &block)
+ end
+
+ def delete(*args, &block)
+ map_method(:delete, *args, &block)
+ end
+
+ def redirect(*args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
- args.push(options)
- case options.delete(:on)
- when :collection
- return collection { match(*args) }
- when :member
- return member { match(*args) }
- end
+ path = args.shift || block
+ path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
+ status = options[:status] || 301
- if @scope[:scope_level] == :resources
- raise ArgumentError, "can't define route directly in resources scope"
- end
+ lambda do |env|
+ req = Rack::Request.new(env)
+ params = path_proc.call(env["action_dispatch.request.path_parameters"])
+ url = req.scheme + '://' + req.host + params
- super
+ [ status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently'] ]
+ end
end
private
- def with_scope_level(kind)
- old, @scope[:scope_level] = @scope[:scope_level], kind
- yield
- ensure
- @scope[:scope_level] = old
+ def map_method(method, *args, &block)
+ options = args.extract_options!
+ options[:via] = method
+ args.push(options)
+ match(*args, &block)
+ self
end
end
module Scoping
+ def initialize(*args)
+ @scope = {}
+ super
+ end
+
def scope(*args)
- options = args.last.is_a?(Hash) ? args.pop : {}
+ options = args.extract_options!
+
+ case args.first
+ when String
+ options[:path] = args.first
+ when Symbol
+ options[:controller] = args.first
+ end
+
+ if path = options.delete(:path)
+ path_set = true
+ path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
+ else
+ path_set = false
+ end
+
+ if name_prefix = options.delete(:name_prefix)
+ name_prefix_set = true
+ name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix)
+ else
+ name_prefix_set = false
+ end
+
+ if controller = options.delete(:controller)
+ controller_set = true
+ controller, @scope[:controller] = @scope[:controller], controller
+ else
+ controller_set = false
+ end
constraints = options.delete(:constraints) || {}
unless constraints.is_a?(Hash)
@@ -148,27 +266,15 @@ module ActionDispatch
options, @scope[:options] = @scope[:options], (@scope[:options] || {}).merge(options)
- path_set = controller_set = false
-
- case args.first
- when String
- path_set = true
- path = args.first
- path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}"
- when Symbol
- controller_set = true
- controller = args.first
- controller, @scope[:controller] = @scope[:controller], controller
- end
-
yield
self
ensure
- @scope[:path] = path if path_set
- @scope[:controller] = controller if controller_set
- @scope[:options] = options
- @scope[:blocks] = blocks
+ @scope[:path] = path if path_set
+ @scope[:name_prefix] = name_prefix if name_prefix_set
+ @scope[:controller] = controller if controller_set
+ @scope[:options] = options
+ @scope[:blocks] = blocks
@scope[:constraints] = constraints
end
@@ -177,151 +283,255 @@ module ActionDispatch
end
def namespace(path)
- scope(path.to_s) { yield }
+ scope("/#{path}") { yield }
end
def constraints(constraints = {})
scope(:constraints => constraints) { yield }
end
- end
- class Constraints
- def initialize(app, constraints = [])
- @app, @constraints = app, constraints
- end
+ def match(*args)
+ options = args.extract_options!
- def call(env)
- req = Rack::Request.new(env)
+ options = (@scope[:options] || {}).merge(options)
- @constraints.each { |constraint|
- if constraint.respond_to?(:matches?) && !constraint.matches?(req)
- return [417, {}, []]
- elsif constraint.respond_to?(:call) && !constraint.call(req)
- return [417, {}, []]
- end
- }
+ if @scope[:name_prefix] && !options[:as].blank?
+ options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
+ elsif @scope[:name_prefix] && options[:as] == ""
+ options[:as] = @scope[:name_prefix].to_s
+ end
- @app.call(env)
+ args.push(options)
+ super(*args)
end
end
- def initialize(set)
- @set = set
- @scope = {}
-
- extend Scoping
- extend Resources
- end
+ module Resources
+ class Resource #:nodoc:
+ attr_reader :plural, :singular
- def get(*args, &block)
- map_method(:get, *args, &block)
- end
+ def initialize(entities, options = {})
+ entities = entities.to_s
- def post(*args, &block)
- map_method(:post, *args, &block)
- end
+ @plural = entities.pluralize
+ @singular = entities.singularize
+ end
- def put(*args, &block)
- map_method(:put, *args, &block)
- end
+ def name
+ plural
+ end
- def delete(*args, &block)
- map_method(:delete, *args, &block)
- end
+ def controller
+ plural
+ end
- def root(options = {})
- match '/', options.merge(:as => :root)
- end
+ def member_name
+ singular
+ end
- def match(*args)
- options = args.last.is_a?(Hash) ? args.pop : {}
+ def collection_name
+ plural
+ end
- if args.length > 1
- args.each { |path| match(path, options) }
- return self
+ def id_segment
+ ":#{singular}_id"
+ end
end
- if args.first.is_a?(Symbol)
- return match(args.first.to_s, options.merge(:to => args.first.to_sym))
+ class SingletonResource < Resource #:nodoc:
+ def initialize(entity, options = {})
+ super
+ end
+
+ def name
+ singular
+ end
end
- path = args.first
+ def resource(*resources, &block)
+ options = resources.extract_options!
- options = (@scope[:options] || {}).merge(options)
- conditions, defaults = {}, {}
+ if resources.length > 1
+ raise ArgumentError if block_given?
+ resources.each { |r| resource(r, options) }
+ return self
+ end
+
+ resource = SingletonResource.new(resources.pop)
- path = nil if path == ""
- path = Rack::Mount::Utils.normalize_path(path) if path
- path = "#{@scope[:path]}#{path}" if @scope[:path]
+ if @scope[:scope_level] == :resources
+ nested do
+ resource(resource.name, options, &block)
+ end
+ return self
+ end
- raise ArgumentError, "path is required" unless path
+ scope(:path => "/#{resource.name}", :controller => resource.controller) do
+ with_scope_level(:resource, resource) do
+ yield if block_given?
- constraints = options[:constraints] || {}
- unless constraints.is_a?(Hash)
- block, constraints = constraints, {}
+ get "(.:format)", :to => :show, :as => resource.member_name
+ post "(.:format)", :to => :create
+ put "(.:format)", :to => :update
+ delete "(.:format)", :to => :destroy
+ get "/new(.:format)", :to => :new, :as => "new_#{resource.singular}"
+ get "/edit(.:format)", :to => :edit, :as => "edit_#{resource.singular}"
+ end
+ end
+
+ self
end
- blocks = ((@scope[:blocks] || []) + [block]).compact
- constraints = (@scope[:constraints] || {}).merge(constraints)
- options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) }
- conditions[:path_info] = path
- requirements = constraints.dup
+ def resources(*resources, &block)
+ options = resources.extract_options!
+
+ if resources.length > 1
+ raise ArgumentError if block_given?
+ resources.each { |r| resources(r, options) }
+ return self
+ end
+
+ resource = Resource.new(resources.pop)
+
+ if @scope[:scope_level] == :resources
+ nested do
+ resources(resource.name, options, &block)
+ end
+ return self
+ end
+
+ scope(:path => "/#{resource.name}", :controller => resource.controller) do
+ with_scope_level(:resources, resource) do
+ yield if block_given?
+
+ with_scope_level(:collection) do
+ get "(.:format)", :to => :index, :as => resource.collection_name
+ post "(.:format)", :to => :create
+
+ with_exclusive_name_prefix :new do
+ get "/new(.:format)", :to => :new, :as => resource.singular
+ end
+ end
+
+ with_scope_level(:member) do
+ scope("/:id") do
+ get "(.:format)", :to => :show, :as => resource.member_name
+ put "(.:format)", :to => :update
+ delete "(.:format)", :to => :destroy
+
+ with_exclusive_name_prefix :edit do
+ get "/edit(.:format)", :to => :edit, :as => resource.singular
+ end
+ end
+ end
+ end
+ end
- path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS)
- segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names
- constraints.reject! { |k, v| segment_keys.include?(k.to_s) }
- conditions.merge!(constraints)
+ self
+ end
- if via = options[:via]
- via = Array(via).map { |m| m.to_s.upcase }
- conditions[:request_method] = Regexp.union(*via)
+ def collection
+ unless @scope[:scope_level] == :resources
+ raise ArgumentError, "can't use collection outside resources scope"
+ end
+
+ with_scope_level(:collection) do
+ scope(:name_prefix => parent_resource.collection_name, :as => "") do
+ yield
+ end
+ end
end
- defaults[:controller] = @scope[:controller].to_s if @scope[:controller]
+ def member
+ unless @scope[:scope_level] == :resources
+ raise ArgumentError, "can't use member outside resources scope"
+ end
- if options[:to].respond_to?(:call)
- app = options[:to]
- defaults.delete(:controller)
- defaults.delete(:action)
- elsif options[:to].is_a?(String)
- defaults[:controller], defaults[:action] = options[:to].split('#')
- elsif options[:to].is_a?(Symbol)
- defaults[:action] = options[:to].to_s
+ with_scope_level(:member) do
+ scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do
+ yield
+ end
+ end
end
- app ||= Routing::RouteSet::Dispatcher.new(:defaults => defaults)
- if app.is_a?(Routing::RouteSet::Dispatcher)
- unless defaults.include?(:controller) || segment_keys.include?("controller")
- raise ArgumentError, "missing :controller"
+ def nested
+ unless @scope[:scope_level] == :resources
+ raise ArgumentError, "can't use nested outside resources scope"
end
- unless defaults.include?(:action) || segment_keys.include?("action")
- raise ArgumentError, "missing :action"
+
+ with_scope_level(:nested) do
+ scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
+ yield
+ end
end
end
- app = Constraints.new(app, blocks) if blocks.any?
- @set.add_route(app, conditions, requirements, defaults, options[:as])
+ def match(*args)
+ options = args.extract_options!
- self
- end
+ if args.length > 1
+ args.each { |path| match(path, options) }
+ return self
+ end
- def redirect(path, options = {})
- status = options[:status] || 301
- lambda { |env|
- req = Rack::Request.new(env)
- url = req.scheme + '://' + req.host + path
- [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']]
- }
- end
+ if args.first.is_a?(Symbol)
+ with_exclusive_name_prefix(args.first) do
+ return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym))
+ end
+ end
- private
- def map_method(method, *args, &block)
- options = args.last.is_a?(Hash) ? args.pop : {}
- options[:via] = method
args.push(options)
- match(*args, &block)
- self
+
+ case options.delete(:on)
+ when :collection
+ return collection { match(*args) }
+ when :member
+ return member { match(*args) }
+ end
+
+ if @scope[:scope_level] == :resources
+ raise ArgumentError, "can't define route directly in resources scope"
+ end
+
+ super
end
+
+ protected
+ def parent_resource
+ @scope[:scope_level_resource]
+ end
+
+ private
+ def with_exclusive_name_prefix(prefix)
+ begin
+ old_name_prefix = @scope[:name_prefix]
+
+ if !old_name_prefix.blank?
+ @scope[:name_prefix] = "#{prefix}_#{@scope[:name_prefix]}"
+ else
+ @scope[:name_prefix] = prefix.to_s
+ end
+
+ yield
+ ensure
+ @scope[:name_prefix] = old_name_prefix
+ end
+ end
+
+ def with_scope_level(kind, resource = parent_resource)
+ old, @scope[:scope_level] = @scope[:scope_level], kind
+ old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
+ yield
+ ensure
+ @scope[:scope_level] = old
+ @scope[:scope_level_resource] = old_resource
+ end
+ end
+
+ include Base
+ include HttpHelpers
+ include Scoping
+ include Resources
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index c28df76f3f..bd397432ce 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -5,7 +5,7 @@ module ActionDispatch
module Routing
class RouteSet #:nodoc:
NotFound = lambda { |env|
- raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect} with #{env.inspect}"
+ raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}"
}
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
@@ -18,31 +18,37 @@ module ActionDispatch
def call(env)
params = env[PARAMETERS_KEY]
+ prepare_params!(params)
+
+ unless controller = controller(params)
+ return [404, {'X-Cascade' => 'pass'}, []]
+ end
+
+ controller.action(params[:action]).call(env)
+ end
+
+ def prepare_params!(params)
merge_default_action!(params)
split_glob_param!(params) if @glob_param
+
params.each do |key, value|
if value.is_a?(String)
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
params[key] = URI.unescape(value)
end
end
+ end
- if env['action_controller.recognize']
- [200, {}, params]
- else
- controller = controller(params)
- controller.action(params[:action]).call(env)
+ def controller(params)
+ if params && params.has_key?(:controller)
+ controller = "#{params[:controller].camelize}Controller"
+ ActiveSupport::Inflector.constantize(controller)
end
+ rescue NameError
+ nil
end
private
- def controller(params)
- if params && params.has_key?(:controller)
- controller = "#{params[:controller].camelize}Controller"
- ActiveSupport::Inflector.constantize(controller)
- end
- end
-
def merge_default_action!(params)
params[:action] ||= 'index'
end
@@ -197,26 +203,40 @@ module ActionDispatch
end
end
- attr_accessor :routes, :named_routes, :configuration_files
+ attr_accessor :routes, :named_routes
+ attr_accessor :disable_clear_and_finalize
def initialize
- self.configuration_files = []
-
self.routes = []
self.named_routes = NamedRouteCollection.new
- clear!
+ @disable_clear_and_finalize = false
end
def draw(&block)
- clear!
- Mapper.new(self).instance_exec(DeprecatedMapper.new(self), &block)
+ clear! unless @disable_clear_and_finalize
+
+ mapper = Mapper.new(self)
+ if block.arity == 1
+ mapper.instance_exec(DeprecatedMapper.new(self), &block)
+ else
+ mapper.instance_exec(&block)
+ end
+
+ finalize! unless @disable_clear_and_finalize
+
+ nil
+ end
+
+ def finalize!
@set.add_route(NotFound)
install_helpers
@set.freeze
end
def clear!
+ # Clear the controller cache so we may discover new ones
+ @controller_constraints = nil
routes.clear
named_routes.clear
@set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY)
@@ -231,63 +251,38 @@ module ActionDispatch
routes.empty?
end
- def add_configuration_file(path)
- self.configuration_files << path
- end
-
- # Deprecated accessor
- def configuration_file=(path)
- add_configuration_file(path)
- end
-
- # Deprecated accessor
- def configuration_file
- configuration_files
- end
+ CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/
- def load!
- Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones
- load_routes!
- end
-
- # reload! will always force a reload whereas load checks the timestamp first
- alias reload! load!
-
- def reload
- if configuration_files.any? && @routes_last_modified
- if routes_changed_at == @routes_last_modified
- return # routes didn't change, don't reload
- else
- @routes_last_modified = routes_changed_at
- end
+ def controller_constraints
+ @controller_constraints ||= begin
+ source = controller_namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" }
+ source << CONTROLLER_REGEXP.source
+ Regexp.compile(source.sort.reverse.join('|'))
end
-
- load!
end
- def load_routes!
- if configuration_files.any?
- configuration_files.each { |config| load(config) }
- @routes_last_modified = routes_changed_at
- else
- draw do |map|
- map.connect ":controller/:action/:id"
- end
- end
- end
+ def controller_namespaces
+ namespaces = Set.new
- def routes_changed_at
- routes_changed_at = nil
-
- configuration_files.each do |config|
- config_changed_at = File.stat(config).mtime
+ # Find any nested controllers already in memory
+ ActionController::Base.subclasses.each do |klass|
+ controller_name = klass.underscore
+ namespaces << controller_name.split('/')[0...-1].join('/')
+ end
- if routes_changed_at.nil? || config_changed_at > routes_changed_at
- routes_changed_at = config_changed_at
+ # TODO: Move this into Railties
+ if defined?(Rails.application)
+ # Find namespaces in controllers/ directory
+ Rails.application.config.controller_paths.each do |load_path|
+ load_path = File.expand_path(load_path)
+ Dir["#{load_path}/**/*_controller.rb"].collect do |path|
+ namespaces << File.dirname(path).sub(/#{load_path}\/?/, '')
+ end
end
end
- routes_changed_at
+ namespaces.delete('')
+ namespaces
end
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil)
@@ -372,14 +367,34 @@ module ActionDispatch
end
recall[:action] = options.delete(:action) if options[:action] == 'index'
- path = _uri(named_route, options, recall)
+ opts = {}
+ opts[:parameterize] = lambda { |name, value|
+ if name == :controller
+ value
+ elsif value.is_a?(Array)
+ value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/')
+ else
+ Rack::Mount::Utils.escape_uri(value.to_param)
+ end
+ }
+
+ unless result = @set.generate(:path_info, named_route, options, recall, opts)
+ raise ActionController::RoutingError, "No route matches #{options.inspect}"
+ end
+
+ path, params = result
+ params.each do |k, v|
+ if v
+ params[k] = v
+ else
+ params.delete(k)
+ end
+ end
+
if path && method == :generate_extras
- uri = URI(path)
- extras = uri.query ?
- Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } :
- []
- [uri.path, extras]
+ [path, params.keys]
elsif path
+ path << "?#{params.to_query}" if params.any?
path
else
raise ActionController::RoutingError, "No route matches #{options.inspect}"
@@ -390,37 +405,11 @@ module ActionDispatch
def call(env)
@set.call(env)
- rescue ActionController::RoutingError => e
- raise e if env['action_controller.rescue_error'] == false
-
- method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO']
-
- # Route was not recognized. Try to find out why (maybe wrong verb).
- allows = HTTP_METHODS.select { |verb|
- begin
- recognize_path(path, {:method => verb}, false)
- rescue ActionController::RoutingError
- nil
- end
- }
-
- if !HTTP_METHODS.include?(method)
- raise ActionController::NotImplemented.new(*allows)
- elsif !allows.empty?
- raise ActionController::MethodNotAllowed.new(*allows)
- else
- raise e
- end
- end
-
- def recognize(request)
- params = recognize_path(request.path, extract_request_environment(request))
- request.path_parameters = params.with_indifferent_access
- "#{params[:controller].to_s.camelize}Controller".constantize
end
- def recognize_path(path, environment = {}, rescue_error = true)
+ def recognize_path(path, environment = {})
method = (environment[:method] || "GET").to_s.upcase
+ path = Rack::Mount::Utils.normalize_path(path)
begin
env = Rack::MockRequest.env_for(path, {:method => method})
@@ -428,70 +417,17 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- env['action_controller.recognize'] = true
- env['action_controller.rescue_error'] = rescue_error
- status, headers, body = call(env)
- body
- end
-
- # Subclasses and plugins may override this method to extract further attributes
- # from the request, for use by route conditions and such.
- def extract_request_environment(request)
- { :method => request.method }
- end
-
- private
- def _uri(named_route, params, recall)
- params = URISegment.wrap_values(params)
- recall = URISegment.wrap_values(recall)
-
- unless result = @set.generate(:path_info, named_route, params, recall)
- return
+ req = Rack::Request.new(env)
+ @set.recognize(req) do |route, params|
+ dispatcher = route.app
+ if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params)
+ dispatcher.prepare_params!(params)
+ return params
end
-
- uri, params = result
- params.each do |k, v|
- if v._value
- params[k] = v._value
- else
- params.delete(k)
- end
- end
-
- uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any?
- uri
end
- class URISegment < Struct.new(:_value, :_escape)
- EXCLUDED = [:controller]
-
- def self.wrap_values(hash)
- hash.inject({}) { |h, (k, v)|
- h[k] = new(v, !EXCLUDED.include?(k.to_sym))
- h
- }
- end
-
- extend Forwardable
- def_delegators :_value, :==, :eql?, :hash
-
- def to_param
- @to_param ||= begin
- if _value.is_a?(Array)
- _value.map { |v| _escaped(v) }.join('/')
- else
- _escaped(_value)
- end
- end
- end
- alias_method :to_s, :to_param
-
- private
- def _escaped(value)
- v = value.respond_to?(:to_param) ? value.to_param : value
- _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s
- end
- end
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
end
end
end