diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r-- | actionpack/lib/action_dispatch/routing/inspector.rb | 121 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 471 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/polymorphic_routes.rb | 32 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/redirection.rb | 174 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 319 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/routes_proxy.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/url_for.rb | 48 |
7 files changed, 777 insertions, 392 deletions
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb new file mode 100644 index 0000000000..bc7229b6a1 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -0,0 +1,121 @@ +require 'delegate' + +module ActionDispatch + module Routing + class RouteWrapper < SimpleDelegator + def endpoint + rack_app ? rack_app.inspect : "#{controller}##{action}" + end + + def constraints + requirements.except(:controller, :action) + end + + def rack_app(app = self.app) + @rack_app ||= begin + class_name = app.class.name.to_s + if class_name == "ActionDispatch::Routing::Mapper::Constraints" + rack_app(app.app) + elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/ + app + end + end + end + + def verb + super.source.gsub(/[$^]/, '') + end + + def path + super.spec.to_s + end + + def name + super.to_s + end + + def reqs + @reqs ||= begin + reqs = endpoint + reqs += " #{constraints.inspect}" unless constraints.empty? + reqs + end + end + + def controller + requirements[:controller] || ':controller' + end + + def action + requirements[:action] || ':action' + end + + def internal? + path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}} + end + + def engine? + rack_app && rack_app.respond_to?(:routes) + end + end + + ## + # This class is just used for displaying route information when someone + # executes `rake routes`. People should not use this class. + class RoutesInspector # :nodoc: + def initialize + @engines = Hash.new + end + + def format(all_routes, filter = nil) + if filter + all_routes = all_routes.select{ |route| route.defaults[:controller] == filter } + end + + routes = collect_routes(all_routes) + + formatted_routes(routes) + + formatted_routes_for_engines + end + + def collect_routes(routes) + routes = routes.collect do |route| + RouteWrapper.new(route) + end.reject do |route| + route.internal? + end.collect do |route| + collect_engine_routes(route) + + {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs } + end + end + + def collect_engine_routes(route) + name = route.endpoint + return unless route.engine? + return if @engines[name] + + routes = route.rack_app.routes + if routes.is_a?(ActionDispatch::Routing::RouteSet) + @engines[name] = collect_routes(routes.routes) + end + end + + def formatted_routes_for_engines + @engines.map do |name, routes| + ["\nRoutes for #{name}:"] + formatted_routes(routes) + end.flatten + end + + def formatted_routes(routes) + name_width = routes.map{ |r| r[:name].length }.max + verb_width = routes.map{ |r| r[:verb].length }.max + path_width = routes.map{ |r| r[:path].length }.max + + routes.map do |r| + "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 970236a05a..f64cff8394 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/inclusion' +require 'active_support/core_ext/hash/reverse_merge' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/enumerable' require 'active_support/inflector' require 'action_dispatch/routing/redirection' @@ -16,7 +17,7 @@ module ActionDispatch end end - attr_reader :app + attr_reader :app, :constraints def initialize(app, constraints, request) @app, @constraints, @request = app, constraints, request @@ -34,6 +35,8 @@ module ActionDispatch } return true + ensure + req.reset_parameters end def call(env) @@ -54,9 +57,20 @@ module ActionDispatch def initialize(set, scope, path, options) @set, @scope = set, scope + @segment_keys = nil @options = (@scope[:options] || {}).merge(options) @path = normalize_path(path) normalize_options! + + via_all = @options.delete(:via) if @options[:via] == :all + + if !via_all && request_method_condition.empty? + msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise msg + end end def to_route @@ -86,6 +100,10 @@ module ActionDispatch raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" end end + + if @options[:constraints].is_a?(Hash) + (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints])) + end end # match "account/overview" @@ -213,7 +231,9 @@ module ActionDispatch end def segment_keys - @segment_keys ||= Journey::Path::Pattern.new( + return @segment_keys if @segment_keys + + @segment_keys = Journey::Path::Pattern.new( Journey::Router::Strexp.compile(@path, requirements, SEPARATORS) ).names end @@ -229,6 +249,11 @@ module ActionDispatch def default_action @options[:action] || @scope[:action] end + + def defaults_from_constraints(constraints) + url_keys = [:protocol, :subdomain, :domain, :host, :port] + constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) } + end end # Invokes Rack::Mount::Utils.normalize path and ensure that @@ -236,12 +261,12 @@ module ActionDispatch # for root cases, where the latter is the correct one. def self.normalize_path(path) path = Journey::Router::Utils.normalize_path(path) - path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$} + path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$} path end def self.normalize_name(name) - normalize_path(name)[1..-1].gsub("/", "_") + normalize_path(name)[1..-1].tr("/", "_") end module Base @@ -251,11 +276,16 @@ module ActionDispatch # # For options, see +match+, as +root+ uses it internally. # + # You can also pass a string which will expand + # + # root 'pages#main' + # # You should put the root route at the top of <tt>config/routes.rb</tt>, # because this means it will be matched first. As this is the most popular route # of most Rails applications, this is beneficial. def root(options = {}) - match '/', { :as => :root }.merge(options) + options = { :to => options } if options.is_a?(String) + match '/', { :as => :root, :via => :get }.merge(options) end # Matches a url pattern to one or more routes. Any symbols in a pattern @@ -325,7 +355,7 @@ module ActionDispatch # +call+ or a string representing a controller's action. # # match 'path', :to => 'controller#action' - # match 'path', :to => lambda { [200, {}, "Success!"] } + # match 'path', :to => lambda { |env| [200, {}, "Success!"] } # match 'path', :to => RackApp # # [:on] @@ -373,11 +403,11 @@ module ActionDispatch # # # Matches any request starting with 'path' # match 'path' => 'c#a', :anchor => false + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. def match(path, options=nil) - mapping = Mapping.new(@set, @scope, path, options || {}) - app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) - self end # Mount a Rack-based application to be used within the application. @@ -403,6 +433,10 @@ module ActionDispatch if options path = options.delete(:at) else + unless Hash === app + raise ArgumentError, "must be called with mount point" + end + options = app app, path = options.find { |k, v| k.respond_to?(:call) } options.delete(app) if app @@ -410,7 +444,8 @@ module ActionDispatch raise "A rack application must be specified" unless path - options[:as] ||= app_name(app) + options[:as] ||= app_name(app) + options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) @@ -437,7 +472,7 @@ module ActionDispatch app.railtie_name else class_name = app.class.is_a?(Class) ? app.name : app.class.name - ActiveSupport::Inflector.underscore(class_name).gsub("/", "_") + ActiveSupport::Inflector.underscore(class_name).tr("/", "_") end end @@ -447,7 +482,11 @@ module ActionDispatch _route = @set.named_routes.routes[name.to_sym] _routes = @set app.routes.define_mounted_helper(name) - app.routes.class_eval do + app.routes.singleton_class.class_eval do + define_method :mounted? do + true + end + define_method :_generate_prefix do |options| prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for @@ -464,49 +503,49 @@ module ActionDispatch # Define a route that only recognizes HTTP GET. # For supported arguments, see <tt>Base#match</tt>. # - # Example: - # - # get 'bacon', :to => 'food#bacon' + # get 'bacon', :to => 'food#bacon' def get(*args, &block) - map_method(:get, *args, &block) + map_method(:get, args, &block) end # Define a route that only recognizes HTTP POST. # For supported arguments, see <tt>Base#match</tt>. # - # Example: - # - # post 'bacon', :to => 'food#bacon' + # post 'bacon', :to => 'food#bacon' def post(*args, &block) - map_method(:post, *args, &block) + map_method(:post, args, &block) end - # Define a route that only recognizes HTTP PUT. + # Define a route that only recognizes HTTP PATCH. # For supported arguments, see <tt>Base#match</tt>. # - # Example: - # - # put 'bacon', :to => 'food#bacon' - def put(*args, &block) - map_method(:put, *args, &block) + # patch 'bacon', :to => 'food#bacon' + def patch(*args, &block) + map_method(:patch, args, &block) end # Define a route that only recognizes HTTP PUT. # For supported arguments, see <tt>Base#match</tt>. # - # Example: + # put 'bacon', :to => 'food#bacon' + def put(*args, &block) + map_method(:put, args, &block) + end + + # Define a route that only recognizes HTTP DELETE. + # For supported arguments, see <tt>Base#match</tt>. # - # delete 'broccoli', :to => 'food#broccoli' + # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) - map_method(:delete, *args, &block) + map_method(:delete, args, &block) end private - def map_method(method, *args, &block) + def map_method(method, args, &block) options = args.extract_options! - options[:via] = method - args.push(options) - match(*args, &block) + options[:via] = method + options[:path] ||= args.first if args.first.is_a?(String) + match(*args, options, &block) self end end @@ -524,13 +563,13 @@ module ActionDispatch # This will create a number of routes for each of the posts and comments # controller. For <tt>Admin::PostsController</tt>, Rails will create: # - # GET /admin/posts - # GET /admin/posts/new - # POST /admin/posts - # GET /admin/posts/1 - # GET /admin/posts/1/edit - # PUT /admin/posts/1 - # DELETE /admin/posts/1 + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PATCH/PUT /admin/posts/1 + # DELETE /admin/posts/1 # # If you want to route /posts (without the prefix /admin) to # <tt>Admin::PostsController</tt>, you could use @@ -558,13 +597,13 @@ module ActionDispatch # not use scope. In the last case, the following paths map to # +PostsController+: # - # GET /admin/posts - # GET /admin/posts/new - # POST /admin/posts - # GET /admin/posts/1 - # GET /admin/posts/1/edit - # PUT /admin/posts/1 - # DELETE /admin/posts/1 + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PATCH/PUT /admin/posts/1 + # DELETE /admin/posts/1 module Scoping # Scopes a set of routes to the given default options. # @@ -610,6 +649,10 @@ module ActionDispatch block, options[:constraints] = options[:constraints], {} end + if options[:constraints].is_a?(Hash) + (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints])) + end + scope_options.each do |option| if value = options.delete(option) recover[option] = @scope[option] @@ -636,7 +679,6 @@ module ActionDispatch # Scopes routes to a specific controller # - # Example: # controller "food" do # match "bacon", :action => "bacon" # end @@ -653,13 +695,13 @@ module ActionDispatch # # This generates the following routes: # - # admin_posts GET /admin/posts(.:format) admin/posts#index - # admin_posts POST /admin/posts(.:format) admin/posts#create - # new_admin_post GET /admin/posts/new(.:format) admin/posts#new - # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit - # admin_post GET /admin/posts/:id(.:format) admin/posts#show - # admin_post PUT /admin/posts/:id(.:format) admin/posts#update - # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy # # === Options # @@ -818,6 +860,11 @@ module ActionDispatch def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end + + def defaults_from_constraints(constraints) + url_keys = [:protocol, :subdomain, :domain, :host, :port] + constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) } + end end # Resource routing allows you to quickly declare all of the common routes @@ -863,24 +910,23 @@ module ActionDispatch # CANONICAL_ACTIONS holds all actions that does not need a prefix or # a path appended since they fit properly in their scope level. VALID_ON_OPTIONS = [:new, :collection, :member] - RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: - DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] - - attr_reader :controller, :path, :options + attr_reader :controller, :path, :options, :param def initialize(entities, options = {}) @name = entities.to_s @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @as = options[:as] + @param = (options[:param] || :id).to_sym @options = options end def default_actions - self.class::DEFAULT_ACTIONS + [:index, :create, :new, :show, :update, :destroy, :edit] end def actions @@ -920,30 +966,37 @@ module ActionDispatch alias :collection_scope :path def member_scope - "#{path}/:id" + "#{path}/:#{param}" end + alias :shallow_scope :member_scope + def new_scope(new_path) "#{path}/#{new_path}" end + def nested_param + :"#{singular}_#{param}" + end + def nested_scope - "#{path}/:#{singular}_id" + "#{path}/:#{nested_param}" end end class SingletonResource < Resource #:nodoc: - DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] - def initialize(entities, options) super - @as = nil @controller = (options[:controller] || plural).to_s @as = options[:as] end + def default_actions + [:show, :create, :update, :destroy, :new, :edit] + end + def plural @plural ||= name.to_s.pluralize end @@ -975,12 +1028,12 @@ module ActionDispatch # the +GeoCoders+ controller (note that the controller is named after # the plural): # - # GET /geocoder/new - # POST /geocoder - # GET /geocoder - # GET /geocoder/edit - # PUT /geocoder - # DELETE /geocoder + # GET /geocoder/new + # POST /geocoder + # GET /geocoder + # GET /geocoder/edit + # PATCH/PUT /geocoder + # DELETE /geocoder # # === Options # Takes same options as +resources+. @@ -991,9 +1044,11 @@ module ActionDispatch return self end - resource_scope(SingletonResource.new(resources.pop, options)) do + resource_scope(:resource, SingletonResource.new(resources.pop, options)) do yield if block_given? + concerns(options[:concerns]) if options[:concerns] + collection do post :create end if parent_resource.actions.include?(:create) @@ -1003,9 +1058,12 @@ module ActionDispatch end if parent_resource.actions.include?(:new) member do - get :edit if parent_resource.actions.include?(:edit) - get :show if parent_resource.actions.include?(:show) - put :update if parent_resource.actions.include?(:update) + get :edit if parent_resource.actions.include?(:edit) + get :show if parent_resource.actions.include?(:show) + if parent_resource.actions.include?(:update) + patch :update + put :update + end delete :destroy if parent_resource.actions.include?(:destroy) end end @@ -1023,13 +1081,13 @@ module ActionDispatch # creates seven different routes in your application, all mapping to # the +Photos+ controller: # - # GET /photos - # GET /photos/new - # POST /photos - # GET /photos/:id - # GET /photos/:id/edit - # PUT /photos/:id - # DELETE /photos/:id + # GET /photos + # GET /photos/new + # POST /photos + # GET /photos/:id + # GET /photos/:id/edit + # PATCH/PUT /photos/:id + # DELETE /photos/:id # # Resources can also be nested infinitely by using this block syntax: # @@ -1039,25 +1097,32 @@ module ActionDispatch # # This generates the following comments routes: # - # GET /photos/:photo_id/comments - # GET /photos/:photo_id/comments/new - # POST /photos/:photo_id/comments - # GET /photos/:photo_id/comments/:id - # GET /photos/:photo_id/comments/:id/edit - # PUT /photos/:photo_id/comments/:id - # DELETE /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments + # GET /photos/:photo_id/comments/new + # POST /photos/:photo_id/comments + # GET /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments/:id/edit + # PATCH/PUT /photos/:photo_id/comments/:id + # DELETE /photos/:photo_id/comments/:id # # === Options # Takes same options as <tt>Base#match</tt> as well as: # # [:path_names] - # Allows you to change the paths of the seven default actions. - # Paths not specified are not changed. + # Allows you to change the segment component of the +edit+ and +new+ actions. + # Actions not specified are not changed. # # resources :posts, :path_names => { :new => "brand_new" } # # The above example will now change /posts/new to /posts/brand_new # + # [:path] + # Allows you to change the path prefix for the resource. + # + # resources :posts, :path => 'postings' + # + # The resource and all segments will now route to /postings instead of /posts + # # [:only] # Only generate routes for the given actions. # @@ -1100,13 +1165,36 @@ module ActionDispatch # # The +comments+ resource here will have the following routes generated for it: # - # post_comments GET /posts/:post_id/comments(.:format) - # post_comments POST /posts/:post_id/comments(.:format) - # new_post_comment GET /posts/:post_id/comments/new(.:format) - # edit_comment GET /sekret/comments/:id/edit(.:format) - # comment GET /sekret/comments/:id(.:format) - # comment PUT /sekret/comments/:id(.:format) - # comment DELETE /sekret/comments/:id(.:format) + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PATCH/PUT /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) + # + # [:shallow_prefix] + # Prefixes nested shallow route names with specified prefix. + # + # scope :shallow_prefix => "sekret" do + # resources :posts do + # resources :comments, :shallow => true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_sekret_comment GET /comments/:id/edit(.:format) + # sekret_comment GET /comments/:id(.:format) + # sekret_comment PATCH/PUT /comments/:id(.:format) + # sekret_comment DELETE /comments/:id(.:format) + # + # [:format] + # Allows you to specify the default value for optional +format+ + # segment or disable it by supplying +false+. # # === Examples # @@ -1122,9 +1210,11 @@ module ActionDispatch return self end - resource_scope(Resource.new(resources.pop, options)) do + resource_scope(:resources, Resource.new(resources.pop, options)) do yield if block_given? + concerns(options[:concerns]) if options[:concerns] + collection do get :index if parent_resource.actions.include?(:index) post :create if parent_resource.actions.include?(:create) @@ -1135,9 +1225,12 @@ module ActionDispatch end if parent_resource.actions.include?(:new) member do - get :edit if parent_resource.actions.include?(:edit) - get :show if parent_resource.actions.include?(:show) - put :update if parent_resource.actions.include?(:update) + get :edit if parent_resource.actions.include?(:edit) + get :show if parent_resource.actions.include?(:show) + if parent_resource.actions.include?(:update) + patch :update + put :update + end delete :destroy if parent_resource.actions.include?(:destroy) end end @@ -1245,32 +1338,47 @@ module ActionDispatch parent_resource.instance_of?(Resource) && @scope[:shallow] end - def match(*args) - options = args.extract_options!.dup - options[:anchor] = true unless options.key?(:anchor) - - if args.length > 1 - args.each { |path| match(path, options.dup) } - return self + # match 'path' => 'controller#action' + # match 'path', to: 'controller#action' + # match 'path', 'otherpath', on: :member, via: :get + def match(path, *rest) + if rest.empty? && Hash === path + options = path + path, to = options.find { |name, value| name.is_a?(String) } + options[:to] = to + options.delete(path) + paths = [path] + else + options = rest.pop || {} + paths = [path] + rest end - on = options.delete(:on) - if VALID_ON_OPTIONS.include?(on) - args.push(options) - return send(on){ match(*args) } - elsif on + options[:anchor] = true unless options.key?(:anchor) + + if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end - if @scope[:scope_level] == :resources - args.push(options) - return nested { match(*args) } - elsif @scope[:scope_level] == :resource - args.push(options) - return member { match(*args) } + paths.each { |_path| decomposed_match(_path, options.dup) } + self + end + + def decomposed_match(path, options) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, options) } + else + case @scope[:scope_level] + when :resources + nested { decomposed_match(path, options) } + when :resource + member { decomposed_match(path, options) } + else + add_route(path, options) + end end + end - action = args.first + def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) if action.to_s =~ /^[\w\/]+$/ @@ -1279,13 +1387,15 @@ module ActionDispatch action = nil end - if options.key?(:as) && !options[:as] + if !options.fetch(:as, true) options.delete(:as) else options[:as] = name_for_action(options[:as], action) end - super(path, options) + mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) end def root(options={}) @@ -1341,7 +1451,7 @@ module ActionDispatch end def scope_action_options? #:nodoc: - @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) + @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) end def scope_action_options #:nodoc: @@ -1349,11 +1459,11 @@ module ActionDispatch end def resource_scope? #:nodoc: - @scope[:scope_level].in?([:resource, :resources]) + [:resource, :resources].include? @scope[:scope_level] end def resource_method_scope? #:nodoc: - @scope[:scope_level].in?([:collection, :member, :new]) + [:collection, :member, :new].include? @scope[:scope_level] end def with_exclusive_scope @@ -1378,8 +1488,8 @@ module ActionDispatch @scope[:scope_level_resource] = old_resource end - def resource_scope(resource) #:nodoc: - with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do + def resource_scope(kind, resource) #:nodoc: + with_scope_level(kind, resource) do scope(parent_resource.resource_scope) do yield end @@ -1387,18 +1497,20 @@ module ActionDispatch end def nested_options #:nodoc: - {}.tap do |options| - options[:as] = parent_resource.member_name - options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? - end + options = { :as => parent_resource.member_name } + options[:constraints] = { + parent_resource.nested_param => param_constraint + } if param_constraint? + + options end - def id_constraint? #:nodoc: - @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp) + def param_constraint? #:nodoc: + @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) end - def id_constraint #:nodoc: - @scope[:constraints][:id] + def param_constraint #:nodoc: + @scope[:constraints][parent_resource.param] end def canonical_action?(action, flag) #:nodoc: @@ -1411,9 +1523,9 @@ module ActionDispatch def path_for_action(action, path) #:nodoc: prefix = shallow_scoping? ? - "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] + "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path] - path = if canonical_action?(action, path.blank?) + if canonical_action?(action, path.blank?) prefix.to_s else "#{prefix}/#{action_path(action, path)}" @@ -1421,8 +1533,7 @@ module ActionDispatch end def action_path(name, path = nil) #:nodoc: - # Ruby 1.8 can't transform empty strings to symbols - name = name.to_sym if name.is_a?(String) && !name.empty? + name = name.to_sym if name.is_a?(String) path || @scope[:path_names][name] || name.to_s end @@ -1461,20 +1572,69 @@ module ActionDispatch [name_prefix, member_name, prefix] end - candidate = name.select(&:present?).join("_").presence - candidate unless as.nil? && @set.routes.find { |r| r.name == candidate } + if candidate = name.select(&:present?).join("_").presence + # If a name was not explicitly given, we check if it is valid + # and return nil in case it isn't. Otherwise, we pass the invalid name + # forward so the underlying router engine treats it and raises an exception. + if as.nil? + candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i + else + candidate + end + end end end - module Shorthand #:nodoc: - def match(path, *rest) - if rest.empty? && Hash === path - options = path - path, to = options.find { |name, value| name.is_a?(String) } - options.merge!(:to => to).delete(path) - super(path, options) - else - super + # Routing Concerns allows you to declare common routes that can be reused + # inside others resources and routes. + # + # concern :commentable do + # resources :comments + # end + # + # concern :image_attachable do + # resources :images, only: :index + # end + # + # These concerns are used in Resources routing: + # + # resources :messages, concerns: [:commentable, :image_attachable] + # + # or in a scope or namespace: + # + # namespace :posts do + # concerns :commentable + # end + module Concerns + # Define a routing concern using a name. + # + # concern :commentable do + # resources :comments + # end + # + # Any routing helpers can be used inside a concern. + def concern(name, &block) + @concerns[name] = block + end + + # Use the named concerns + # + # resources :posts do + # concerns :commentable + # end + # + # concerns also work in any routes helper that you want to use: + # + # namespace :posts do + # concerns :commentable + # end + def concerns(*names) + names.flatten.each do |name| + if concern = @concerns[name] + instance_eval(&concern) + else + raise ArgumentError, "No concern named #{name} was found!" + end end end end @@ -1482,14 +1642,15 @@ module ActionDispatch def initialize(set) #:nodoc: @set = set @scope = { :path_names => @set.resources_path_names } + @concerns = {} end include Base include HttpHelpers include Redirection include Scoping + include Concerns include Resources - include Shorthand end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index e989a38d8b..3d7b8878b8 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -1,3 +1,5 @@ +require 'action_controller/model_naming' + module ActionDispatch module Routing # Polymorphic URL helpers are methods for smart resolution to a named route call when @@ -43,18 +45,18 @@ module ActionDispatch # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" # - # == Using with mounted engines - # - # If you use mounted engine, there is a possibility that you will need to use - # polymorphic_url pointing at engine's routes. To do that, just pass proxy used - # to reach engine's routes as a first argument: + # == Usage with mounted engines # - # For example: + # If you are using a mounted engine and you need to use a polymorphic_url + # pointing at the engine's routes, pass in the engine's route proxy as the first + # argument to the method. For example: # - # polymorphic_url([blog, @post]) # it will call blog.post_path(@post) - # form_for([blog, @post]) # => "/blog/posts/1 + # polymorphic_url([blog, @post]) # calls blog.post_path(@post) + # form_for([blog, @post]) # => "/blog/posts/1" # module PolymorphicRoutes + include ActionController::ModelNaming + # Constructs a call to a named RESTful route for the given record and returns the # resulting URL string. For example: # @@ -97,7 +99,7 @@ module ActionDispatch end record = extract_record(record_or_hash_or_array) - record = record.to_model if record.respond_to?(:to_model) + record = convert_to_model(record) args = Array === record_or_hash_or_array ? record_or_hash_or_array.dup : @@ -124,6 +126,8 @@ module ActionDispatch args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options end + args.collect! { |a| convert_to_model(a) } + (proxy || self).send(named_route, *args) end @@ -165,7 +169,7 @@ module ActionDispatch if parent.is_a?(Symbol) || parent.is_a?(String) parent else - ActiveModel::Naming.route_key(parent).singularize + model_name_from_record_or_class(parent).singular_route_key end end else @@ -176,9 +180,11 @@ module ActionDispatch if record.is_a?(Symbol) || record.is_a?(String) route << record elsif record - route << ActiveModel::Naming.route_key(record) - route = [route.join("_").singularize] if inflection == :singular - route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + if inflection == :singular + route << model_name_from_record_or_class(record).singular_route_key + else + route << model_name_from_record_or_class(record).route_key + end else raise ArgumentError, "Nil location provided. Can't build URI." end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 804991ad5f..205ff44b1c 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -1,7 +1,99 @@ require 'action_dispatch/http/request' +require 'active_support/core_ext/uri' +require 'active_support/core_ext/array/extract_options' +require 'rack/utils' +require 'action_controller/metal/exceptions' module ActionDispatch module Routing + class Redirect # :nodoc: + attr_reader :status, :block + + def initialize(status, block) + @status = status + @block = block + end + + def call(env) + req = Request.new(env) + + # If any of the path parameters has a invalid encoding then + # raise since it's likely to trigger errors further on. + req.symbolized_path_parameters.each do |key, value| + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + + uri = URI.parse(path(req.symbolized_path_parameters, req)) + uri.scheme ||= req.scheme + uri.host ||= req.host + uri.port ||= req.port unless req.standard_port? + + body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + + headers = { + 'Location' => uri.to_s, + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + + [ status, headers, [body] ] + end + + def path(params, request) + block.call params, request + end + + def inspect + "redirect(#{status})" + end + end + + class PathRedirect < Redirect + def path(params, request) + (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params)) + end + + def inspect + "redirect(#{status}, #{block})" + end + + private + def escape(params) + Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + end + end + + class OptionRedirect < Redirect # :nodoc: + alias :options :block + + def path(params, request) + url_options = { + :protocol => request.protocol, + :host => request.host, + :port => request.optional_port, + :path => request.path, + :params => request.query_parameters + }.merge options + + if !params.empty? && url_options[:path].match(/%\{\w*\}/) + url_options[:path] = (url_options[:path] % escape_path(params)) + end + + ActionDispatch::Http::URL.url_for url_options + end + + def inspect + "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})" + end + + private + def escape_path(params) + Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }] + end + end + module Redirection # Redirect any path to another path: @@ -19,10 +111,13 @@ module ActionDispatch # params, depending of how many arguments your block accepts. A string is required as a # return value. # - # match 'jokes/:number', :to => redirect do |params, request| - # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp") + # match 'jokes/:number', :to => redirect { |params, request| + # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp") # "http://#{request.host_with_port}/#{path}" - # end + # } + # + # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass + # the block to +match+ instead of +redirect+. Use <tt>{ ... }</tt> instead. # # The options version of redirect allows you to supply only the parts of the url which need # to change, it also supports interpolation of the path similar to the first example. @@ -37,74 +132,17 @@ module ActionDispatch # match 'accounts/:name' => redirect(SubdomainRedirector.new('api')) # def redirect(*args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} + options = args.extract_options! status = options.delete(:status) || 301 + path = args.shift - path = args.shift - - path_proc = if path.is_a?(String) - proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) } - elsif options.any? - options_proc(options) - elsif path.respond_to?(:call) - proc { |params, request| path.call(params, request) } - elsif block - block - else - raise ArgumentError, "redirection argument not supported" - end + return OptionRedirect.new(status, options) if options.any? + return PathRedirect.new(status, path) if String === path - redirection_proc(status, path_proc) + block = path if path.respond_to? :call + raise ArgumentError, "redirection argument not supported" unless block + Redirect.new status, block end - - private - - def options_proc(options) - proc do |params, request| - path = if options[:path].nil? - request.path - elsif params.empty? || !options[:path].match(/%\{\w*\}/) - options.delete(:path) - else - (options.delete(:path) % params) - end - - default_options = { - :protocol => request.protocol, - :host => request.host, - :port => request.optional_port, - :path => path, - :params => request.query_parameters - } - - ActionDispatch::Http::URL.url_for(options.reverse_merge(default_options)) - end - end - - def redirection_proc(status, path_proc) - lambda do |env| - req = Request.new(env) - - params = [req.symbolized_path_parameters] - params << req if path_proc.arity > 1 - - uri = URI.parse(path_proc.call(*params)) - uri.scheme ||= req.scheme - uri.host ||= req.host - uri.port ||= req.port unless req.standard_port? - - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) - - headers = { - 'Location' => uri.to_s, - 'Content-Type' => 'text/html', - 'Content-Length' => body.length.to_s - } - - [ status, headers, [body] ] - end - end - end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 2bcde16110..32d267d1d6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,6 +1,5 @@ require 'journey' require 'forwardable' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' @@ -9,6 +8,12 @@ require 'action_controller/metal/exceptions' module ActionDispatch module Routing class RouteSet #:nodoc: + # Since the router holds references to many parts of the system + # like engines, controllers and the application itself, inspecting + # the route set can actually be really slow, therefore we default + # alias inspect to to_s. + alias inspect to_s + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' class Dispatcher #:nodoc: @@ -20,6 +25,15 @@ module ActionDispatch def call(env) params = env[PARAMETERS_KEY] + + # If any of the path parameters has a invalid encoding then + # raise since it's likely to trigger errors further on. + params.each do |key, value| + unless value.valid_encoding? + raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + end + end + prepare_params!(params) # Just raise undefined constant errors if a controller was specified as default. @@ -31,6 +45,7 @@ module ActionDispatch end def prepare_params!(params) + normalize_controller!(params) merge_default_action!(params) split_glob_param!(params) if @glob_param end @@ -66,6 +81,10 @@ module ActionDispatch controller.action(action).call(env) end + def normalize_controller!(params) + params[:controller] = params[:controller].underscore if params.key?(:controller) + end + def merge_default_action!(params) params[:action] ||= 'index' end @@ -83,7 +102,27 @@ module ActionDispatch attr_reader :routes, :helpers, :module def initialize - clear! + @routes = {} + @helpers = [] + @module = Module.new do + protected + + def handle_positional_args(args, options, segment_keys) + inner_options = args.extract_options! + result = options.dup + + if args.size > 0 + keys = segment_keys + if args.size < keys.size - 1 # take format into account + keys -= self.url_options.keys if self.respond_to?(:url_options) + keys -= options.keys + end + result.merge!(Hash[keys.zip(args)]) + end + + result.merge!(inner_options) + end + end end def helper_names @@ -91,12 +130,8 @@ module ActionDispatch end def clear! - @routes = {} - @helpers = [] - - @module ||= Module.new do - instance_methods.each { |selector| remove_method(selector) } - end + @routes.clear + @helpers.clear end def add(name, route) @@ -125,58 +160,13 @@ module ActionDispatch routes.length end - def reset! - old_routes = routes.dup - clear! - old_routes.each do |name, route| - add(name, route) - end - end - - def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) - reset! if regenerate - Array(destinations).each do |dest| - dest.__send__(:include, @module) - end - end - private - def url_helper_name(name, kind = :url) - :"#{name}_#{kind}" - end - - def hash_access_name(name, kind = :url) - :"hash_for_#{name}_#{kind}" - end def define_named_route_methods(name, route) - {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts| - hash = route.defaults.merge(:use_route => name).merge(opts) - define_hash_access route, name, kind, hash - define_url_helper route, name, kind, hash - end - end - - def define_hash_access(route, name, kind, options) - selector = hash_access_name(name, kind) - - # We use module_eval to avoid leaks - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - options = args.extract_options! - result = #{options.inspect} - - if args.any? - result[:_positional_args] = args - result[:_positional_keys] = #{route.segment_keys.inspect} - end - - result.merge(options) - end - protected :#{selector} - END_EVAL - helpers << selector + define_url_helper route, :"#{name}_path", + route.defaults.merge(:use_route => name, :only_path => true) + define_url_helper route, :"#{name}_url", + route.defaults.merge(:use_route => name, :only_path => false) end # Create a url helper allowing ordered parameters to be associated @@ -192,23 +182,51 @@ module ActionDispatch # # foo_url(bar, baz, bang, :sort_by => 'baz') # - def define_url_helper(route, name, kind, options) - selector = url_helper_name(name, kind) - hash_access_method = hash_access_name(name, kind) - + def define_url_helper(route, name, options) @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - url_for(#{hash_access_method}(*args)) + remove_possible_method :#{name} + def #{name}(*args) + if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation? + options = #{options.inspect} + options.merge!(url_options) if respond_to?(:url_options) + options[:path] = "#{optimized_helper(route)}" + ActionDispatch::Http::URL.url_for(options) + else + url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect})) + end end END_EVAL - helpers << selector + + helpers << name + end + + # Clause check about when we need to generate an optimized helper. + def optimize_helper?(route) #:nodoc: + route.requirements.except(:controller, :action).empty? + end + + # Generates the interpolation to be used in the optimized helper. + def optimized_helper(route) + string_route = route.ast.to_s + + while string_route.gsub!(/\([^\)]*\)/, "") + true + end + + route.required_parts.each_with_index do |part, i| + # Replace each route parameter + # e.g. :id for regular parameter or *path for globbing + # with ruby string interpolation code + string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}") + end + + string_route end end attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names - attr_accessor :default_url_options, :request_class, :valid_conditions + attr_accessor :default_url_options, :request_class alias :routes :set @@ -220,17 +238,7 @@ module ActionDispatch self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names.dup self.default_url_options = {} - self.request_class = request_class - @valid_conditions = {} - - request_class.public_instance_methods.each { |m| - @valid_conditions[m.to_sym] = true - } - @valid_conditions[:controller] = true - @valid_conditions[:action] = true - - self.valid_conditions.delete(:id) @append = [] @prepend = [] @@ -262,8 +270,7 @@ module ActionDispatch def eval_block(block) if block.arity == 1 raise "You are using the old router DSL which has been removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/ " << - "or add the rails_legacy_mapper gem to your Gemfile" + "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" end mapper = Mapper.new(self) if default_scope @@ -287,14 +294,15 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) - Array(destinations).each { |d| d.module_eval { include Helpers } } - named_routes.install(destinations, regenerate_code) - end - - module MountedHelpers + module MountedHelpers #:nodoc: + extend ActiveSupport::Concern + include UrlFor end + # Contains all the mounted helpers accross different + # engines and the `main_app` helper for the application. + # You can include this in your classes if you want to + # access routes for other engines. def mounted_helpers MountedHelpers end @@ -305,11 +313,11 @@ module ActionDispatch routes = self MountedHelpers.class_eval do define_method "_#{name}" do - RoutesProxy.new(routes, self._routes_context) + RoutesProxy.new(routes, _routes_context) end end - MountedHelpers.class_eval <<-RUBY + MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def #{name} @#{name} ||= _#{name} end @@ -320,28 +328,36 @@ module ActionDispatch @url_helpers ||= begin routes = self - helpers = Module.new do + Module.new do extend ActiveSupport::Concern include UrlFor + # Define url_for in the singleton level so one can do: + # Rails.application.routes.url_helpers.url_for(args) @_routes = routes class << self - delegate :url_for, :to => '@_routes' + delegate :url_for, :optimize_routes_generation?, :to => '@_routes' end + + # Make named_routes available in the module singleton + # as well, so one can do: + # Rails.application.routes.url_helpers.posts_path extend routes.named_routes.module - # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that - # we can include? - # Yes plz - JP + # Any class that includes this module will get all + # named routes... + include routes.named_routes.module + + # plus a singleton class method called _routes ... included do - routes.install_helpers(self) singleton_class.send(:redefine_method, :_routes) { routes } end + # And an instance method _routes. Note that + # UrlFor (included in this module) add extra + # conveniences for working with @_routes. define_method(:_routes) { @_routes || routes } end - - helpers end end @@ -353,10 +369,10 @@ module ActionDispatch raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) - conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym }) + conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) - named_routes[name] = route if name + named_routes[name] = route if name && !named_routes[name] route end @@ -367,25 +383,45 @@ module ActionDispatch SEPARATORS, anchor) - Journey::Path::Pattern.new(strexp) + pattern = Journey::Path::Pattern.new(strexp) + + builder = Journey::GTG::Builder.new pattern.spec + + # Get all the symbol nodes followed by literals that are not the + # dummy node. + symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| + builder.followpos(n).first.literal? + } + + # Get all the symbol nodes preceded by literals. + symbols.concat pattern.spec.find_all(&:literal?).map { |n| + builder.followpos(n).first + }.find_all(&:symbol?) + + symbols.each { |x| + x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + } + + pattern end private :build_path - def build_conditions(current_conditions, req_predicates, path_values) + def build_conditions(current_conditions, path_values) conditions = current_conditions.dup - verbs = conditions[:request_method] || [] - # Rack-Mount requires that :request_method be a regular expression. # :request_method represents the HTTP verb that matches this route. # # Here we munge values before they get sent on to rack-mount. + verbs = conditions[:request_method] || [] unless verbs.empty? conditions[:request_method] = %r[^#{verbs.join('|')}$] end - conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) } - conditions + conditions.keep_if do |k, _| + k == :action || k == :controller || + @request_class.public_method_defined?(k) || path_values.include?(k) + end end private :build_conditions @@ -412,12 +448,12 @@ module ActionDispatch normalize_options! normalize_controller_action_id! use_relative_controller! - controller.sub!(%r{^/}, '') if controller + normalize_controller! handle_nil_action! end def controller - @controller ||= @options[:controller] + @options[:controller] end def current_controller @@ -426,9 +462,7 @@ module ActionDispatch def use_recall_for(key) if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) - if named_route_exists? - @options[key] = @recall.delete(key) if segment_keys.include?(key) - else + if !named_route_exists? || segment_keys.include?(key) @options[key] = @recall.delete(key) end end @@ -470,14 +504,19 @@ module ActionDispatch # if the current controller is "foo/bar/baz" and :controller => "baz/bat" # is specified, the controller becomes "foo/baz/bat" def use_relative_controller! - if !named_route && different_controller? + if !named_route && different_controller? && !controller.start_with?("/") old_parts = current_controller.split('/') size = controller.count("/") + 1 parts = old_parts[0...-size] << controller - @controller = @options[:controller] = parts.join("/") + @options[:controller] = parts.join("/") end end + # Remove leading slashes from controllers + def normalize_controller! + @options[:controller] = controller.sub(%r{^/}, '') if controller + end + # This handles the case of :action => nil being explicitly passed. # It is identical to :action => "index" def handle_nil_action! @@ -533,32 +572,44 @@ module ActionDispatch end RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, - :trailing_slash, :anchor, :params, :only_path, :script_name] + :trailing_slash, :anchor, :params, :only_path, :script_name, + :original_script_name] + + def mounted? + false + end + + def optimize_routes_generation? + !mounted? && default_url_options.empty? + end def _generate_prefix(options = {}) nil end + # The +options+ argument must be +nil+ or a hash whose keys are *symbols*. def url_for(options) - finalize! - options = (options || {}).reverse_merge!(default_url_options) - - handle_positional_args(options) + options = default_url_options.merge(options || {}) user, password = extract_authentication(options) - path_segments = options.delete(:_path_segments) - script_name = options.delete(:script_name) + recall = options.delete(:_recall) + + original_script_name = options.delete(:original_script_name).presence + script_name = options.delete(:script_name).presence || _generate_prefix(options) - path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s + if script_name && original_script_name + script_name = original_script_name + script_name + end path_options = options.except(*RESERVED_OPTIONS) path_options = yield(path_options) if block_given? - path_addition, params = generate(path_options, path_segments || {}) - path << path_addition + path, params = generate(path_options, recall || {}) + params.merge!(options[:params] || {}) - ActionDispatch::Http::URL.url_for(options.merge({ + ActionDispatch::Http::URL.url_for(options.merge!({ :path => path, + :script_name => script_name, :params => params, :user => user, :password => password @@ -566,13 +617,13 @@ module ActionDispatch end def call(env) - finalize! @router.call(env) end def recognize_path(path, environment = {}) method = (environment[:method] || "GET").to_s.upcase path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://} + extras = environment[:extras] || {} begin env = Rack::MockRequest.env_for(path, {:method => method}) @@ -582,21 +633,27 @@ module ActionDispatch req = @request_class.new(env) @router.recognize(req) do |route, matches, params| + params.merge!(extras) params.each do |key, value| if value.is_a?(String) - value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware? + value = value.dup.force_encoding(Encoding::BINARY) params[key] = URI.parser.unescape(value) end end - + old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) dispatcher = route.app while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do dispatcher = dispatcher.app end - if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false) - dispatcher.prepare_params!(params) - return params + if dispatcher.is_a?(Dispatcher) + if dispatcher.controller(params, false) + dispatcher.prepare_params!(params) + return params + else + raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" + end end end @@ -613,16 +670,6 @@ module ActionDispatch end end - def handle_positional_args(options) - return unless args = options.delete(:_positional_args) - - keys = options.delete(:_positional_keys) - keys -= options.keys if args.size < keys.size - 1 # take format into account - - # Tell url_for to skip default_url_options - options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }]) - end - end end end diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index f7d5f6397d..73af5920ed 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -16,6 +16,10 @@ module ActionDispatch end end + def respond_to?(method, include_private = false) + super || routes.url_helpers.respond_to?(method) + end + def method_missing(method, *args) if routes.url_helpers.respond_to?(method) self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 8fc8dc191b..f4c708ea33 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -8,7 +8,8 @@ module ActionDispatch # # <b>Tip:</b> If you need to generate URLs from your models or some other place, # then ActionController::UrlFor is what you're looking for. Read on for - # an introduction. + # an introduction. In general, this module should not be included on its own, + # as it is usually included by url_helpers (as in Rails.application.routes.url_helpers). # # == URL generation from parameters # @@ -42,7 +43,7 @@ module ActionDispatch # url_for(:controller => 'users', # :action => 'new', # :message => 'Welcome!', - # :host => 'www.example.com') # Changed this. + # :host => 'www.example.com') # # => "http://www.example.com/users/new?message=Welcome%21" # # By default, all controllers and views have access to a special version of url_for, @@ -52,7 +53,7 @@ module ActionDispatch # # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' - # in full. However, mailers don't have hostname information, and what's why you'll still + # in full. However, mailers don't have hostname information, and that's why you'll still # have to specify the <tt>:host</tt> argument when generating URLs in mailers. # # @@ -67,7 +68,7 @@ module ActionDispatch # This generates, among other things, the method <tt>users_path</tt>. By default, # this method is accessible from your controllers, views and mailers. If you need # to access this auto-generated method from other places (such as a model), then - # you can do that by including ActionController::UrlFor in your class: + # you can do that by including Rails.application.routes.url_helpers in your class: # # class User < ActiveRecord::Base # include Rails.application.routes.url_helpers @@ -84,14 +85,12 @@ module ActionDispatch include PolymorphicRoutes included do - # TODO: with_routing extends @controller with url_helpers, trickling down to including this module which overrides its default_url_options unless method_defined?(:default_url_options) # Including in a class uses an inheritable hash. Modules get a plain hash. if respond_to?(:class_attribute) class_attribute :default_url_options else - mattr_accessor :default_url_options - remove_method :default_url_options + mattr_writer :default_url_options end self.default_url_options = {} @@ -103,6 +102,9 @@ module ActionDispatch super end + # Hook overridden in controller to add request information + # with `default_url_options`. Application logic should not + # go into url_options. def url_options default_url_options end @@ -130,8 +132,6 @@ module ActionDispatch # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to # +url_for+ is forwarded to the Routes module. # - # Examples: - # # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080' # # => 'http://somehost.org:8080/tasks/testing' # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true @@ -142,26 +142,34 @@ module ActionDispatch # # => 'http://somehost.org/tasks/testing?number=33' def url_for(options = nil) case options + when nil + _routes.url_for(url_options.symbolize_keys) + when Hash + _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) when String options - when nil, Hash - _routes.url_for((options || {}).reverse_merge(url_options).symbolize_keys) else polymorphic_url(options) end end protected - def _with_routes(routes) - old_routes, @_routes = @_routes, routes - yield - ensure - @_routes = old_routes - end - def _routes_context - self - end + def optimize_routes_generation? + return @_optimized_routes if defined?(@_optimized_routes) + @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty? + end + + def _with_routes(routes) + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end + + def _routes_context + self + end end end end |