diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2010-01-04 03:24:39 +0530 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2010-01-04 03:24:39 +0530 |
commit | cda36a0731f14b33a920bf7e32255661e06f890a (patch) | |
tree | 79ccba37953f9fe3055503be42b1610faa6d64ad /actionpack/lib/action_dispatch/routing | |
parent | bd4a3cce4ecd8e648179a91e26506e3622ac2162 (diff) | |
parent | a115b5d79a850bb56cd3c9db9a05d6da35e3d7be (diff) | |
download | rails-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.rb | 5 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 624 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 260 |
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 |