diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r-- | actionpack/lib/action_dispatch/routing/inspector.rb | 97 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 900 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/polymorphic_routes.rb | 270 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/redirection.rb | 46 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 389 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/routes_proxy.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/url_for.rb | 58 |
7 files changed, 951 insertions, 813 deletions
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 48c10a7d4c..b91ffb8419 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -1,5 +1,5 @@ -require 'delegate' -require 'active_support/core_ext/string/strip' +require "delegate" +require "active_support/core_ext/string/strip" module ActionDispatch module Routing @@ -16,10 +16,6 @@ module ActionDispatch app.app end - def verb - super.source.gsub(/[$^]/, '') - end - def path super.spec.to_s end @@ -37,15 +33,15 @@ module ActionDispatch end def controller - requirements[:controller] || ':controller' + parts.include?(:controller) ? ":controller" : requirements[:controller] end def action - requirements[:action] || ':action' + parts.include?(:action) ? ":action" : requirements[:action] end def internal? - controller.to_s =~ %r{\Arails/(info|mailers|welcome)} + internal end def engine? @@ -55,7 +51,7 @@ module ActionDispatch ## # This class is just used for displaying route information when someone - # executes `rake routes` or looks at the RoutingError page. + # executes `rails routes` or looks at the RoutingError page. # People should not use this class. class RoutesInspector # :nodoc: def initialize(routes) @@ -64,12 +60,10 @@ module ActionDispatch end def format(formatter, filter = nil) - routes_to_display = filter_routes(filter) - + routes_to_display = filter_routes(normalize_filter(filter)) routes = collect_routes(routes_to_display) - if routes.none? - formatter.no_routes + formatter.no_routes(collect_routes(@routes)) return formatter.result end @@ -86,37 +80,48 @@ module ActionDispatch private - def filter_routes(filter) - if filter - @routes.select { |route| route.defaults[:controller] == filter } - else - @routes + def normalize_filter(filter) + if filter.is_a?(Hash) && filter[:controller] + { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } + elsif filter + { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ } + end + end + + def filter_routes(filter) + if filter + @routes.select do |route| + route_wrapper = RouteWrapper.new(route) + filter.any? { |default, value| route_wrapper.send(default) =~ value } + end + else + @routes + end end - end - def collect_routes(routes) - routes.collect do |route| - RouteWrapper.new(route) - end.reject(&:internal?).collect do |route| - collect_engine_routes(route) + def collect_routes(routes) + routes.collect do |route| + RouteWrapper.new(route) + end.reject(&:internal?).collect do |route| + collect_engine_routes(route) - { name: route.name, - verb: route.verb, - path: route.path, - reqs: route.reqs } + { name: route.name, + verb: route.verb, + path: route.path, + reqs: route.reqs } + end end - end - def collect_engine_routes(route) - name = route.endpoint - return unless route.engine? - return if @engines[name] + 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) + routes = route.rack_app.routes + if routes.is_a?(ActionDispatch::Routing::RouteSet) + @engines[name] = collect_routes(routes.routes) + end end - end end class ConsoleFormatter @@ -140,19 +145,23 @@ module ActionDispatch @buffer << draw_header(routes) end - def no_routes - @buffer << <<-MESSAGE.strip_heredoc + def no_routes(routes) + @buffer << + if routes.none? + <<-MESSAGE.strip_heredoc You don't have any routes defined! Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. MESSAGE + else + "No routes were found for this controller" + end + @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end private def draw_section(routes) - header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) + header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length) name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) routes.map do |r| @@ -191,7 +200,7 @@ module ActionDispatch def header(routes) end - def no_routes + def no_routes(*) @buffer << <<-MESSAGE.strip_heredoc <p>You don't have any routes defined!</p> <ul> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 15980db39b..4efde09b8b 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,22 +1,21 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/module/remove_method' -require 'active_support/inflector' -require 'action_dispatch/routing/redirection' -require 'action_dispatch/routing/endpoint' +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/enumerable" +require "active_support/core_ext/array/extract_options" +require "action_dispatch/routing/redirection" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - class Constraints < Endpoint #:nodoc: + class Constraints < Routing::Endpoint #:nodoc: attr_reader :app, :constraints - def initialize(app, constraints, dispatcher_p) + SERVE = ->(app, req) { app.serve req } + CALL = ->(app, req) { app.call req.env } + + def initialize(app, constraints, strategy) # Unwrap Constraints objects. I don't actually think it's possible # to pass a Constraints object to this constructor, but there were # multiple places that kept testing children of this object. I @@ -26,12 +25,12 @@ module ActionDispatch app = app.app end - @dispatcher = dispatcher_p + @strategy = strategy @app, @constraints, = app, constraints end - def dispatcher?; @dispatcher; end + def dispatcher?; @strategy == SERVE; end def matches?(req) @constraints.all? do |constraint| @@ -41,13 +40,9 @@ module ActionDispatch end def serve(req) - return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req) - if dispatcher? - @app.serve req - else - @app.call req.env - end + @strategy.call @app, req end private @@ -59,101 +54,180 @@ module ActionDispatch class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - attr_reader :requirements, :conditions, :defaults - attr_reader :to, :default_controller, :default_action, :as, :anchor + attr_reader :requirements, :defaults + attr_reader :to, :default_controller, :default_action + attr_reader :required_defaults, :ast - def self.build(scope, set, path, as, options) + def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) options = scope[:options].merge(options) if scope[:options] - options.delete :only - options.delete :except - options.delete :shallow_path - options.delete :shallow_prefix - options.delete :shallow + defaults = (scope[:defaults] || {}).dup + scope_constraints = scope[:constraints] || {} + + new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options + end + + def self.check_via(via) + if via.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 both GET and POST, add `via: [:get, :post]` option.\n" \ + "If you want to expose your action to GET, use `get` in the router:\n" \ + " Instead of: match \"controller#action\"\n" \ + " Do: get \"controller#action\"" + raise ArgumentError, msg + end + via + end + + def self.normalize_path(path, format) + path = Mapper.normalize_path(path) - defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} + if format == true + "#{path}.:format" + elsif optional_format?(path, format) + "#{path}(.:format)" + else + path + end + end - new scope, set, path, defaults, as, options + def self.optional_format?(path, format) + format != false && !path.include?(":format") && !path.end_with?("/") end - def initialize(scope, set, path, defaults, as, options) - @requirements, @conditions = {}, {} + def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) @defaults = defaults @set = set - @to = options.delete :to - @default_controller = options.delete(:controller) || scope[:controller] - @default_action = options.delete(:action) || scope[:action] - @as = as - @anchor = options.delete :anchor + @to = to + @default_controller = controller + @default_action = default_action + @ast = ast + @anchor = anchor + @via = via + @internal = options.delete(:internal) - formatted = options.delete :format - via = Array(options.delete(:via) { [] }) - options_constraints = options.delete :constraints + path_params = ast.find_all(&:symbol?).map(&:to_sym) - path = normalize_path! path, formatted - ast = path_ast path - path_params = path_params ast + options = add_wildcard_options(options, formatted, ast) - options = normalize_options!(options, formatted, path_params, ast, scope[:module]) + options = normalize_options!(options, path_params, modyoule) + split_options = constraints(options, path_params) - split_constraints(path_params, scope[:constraints]) if scope[:constraints] - constraints = constraints(options, path_params) + constraints = scope_constraints.merge Hash[split_options[:constraints] || []] - split_constraints path_params, constraints + if options_constraints.is_a?(Hash) + @defaults = Hash[options_constraints.find_all { |key, default| + URL_OPTIONS.include?(key) && (String === default || Integer === default) + }].merge @defaults + @blocks = blocks + constraints.merge! options_constraints + else + @blocks = blocks(options_constraints) + end - @blocks = blocks(options_constraints, scope[:blocks]) + requirements, conditions = split_constraints path_params, constraints + verify_regexp_requirements requirements.map(&:last).grep(Regexp) - if options_constraints.is_a?(Hash) - split_constraints path_params, options_constraints - options_constraints.each do |key, default| - if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default - end - end + formats = normalize_format(formatted) + + @requirements = formats[:requirements].merge Hash[requirements] + @conditions = Hash[conditions] + @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options)) + + if path_params.include?(:action) && !@requirements.key?(:action) + @defaults[:action] ||= "index" end - normalize_format!(formatted) + @required_defaults = (split_options[:required_defaults] || []).map(&:first) + end - @conditions[:path_info] = path - @conditions[:parsed_path_info] = ast + def make_route(name, precedence) + route = Journey::Route.new(name, + application, + path, + conditions, + required_defaults, + defaults, + request_method, + precedence, + @internal) + + route + end - add_request_method(via, @conditions) - normalize_defaults!(options) + def application + app(@blocks) end - def to_route - [ app(@blocks), conditions, requirements, defaults, as, anchor ] + def path + build_path @ast, requirements, @anchor end - private + def conditions + build_conditions @conditions, @set.request_class + end - def normalize_path!(path, format) - path = Mapper.normalize_path(path) + def build_conditions(current_conditions, request_class) + conditions = current_conditions.dup - if format == true - "#{path}.:format" - elsif optional_format?(path, format) - "#{path}(.:format)" + conditions.keep_if do |k, _| + request_class.public_method_defined?(k) + end + end + private :build_conditions + + def request_method + @via.map { |x| Journey::Route.verb_matcher(x) } + end + private :request_method + + JOINED_SEPARATORS = SEPARATORS.join # :nodoc: + + def build_path(ast, requirements, anchor) + pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor) + + # Find all the symbol nodes that are adjacent to literal nodes and alter + # the regexp so that Journey will partition them into custom routes. + ast.find_all { |node| + next unless node.cat? + + if node.left.literal? && node.right.symbol? + symbol = node.right + elsif node.left.literal? && node.right.cat? && node.right.left.symbol? + symbol = node.right.left + elsif node.left.symbol? && node.right.literal? + symbol = node.left + elsif node.left.symbol? && node.right.cat? && node.right.left.literal? + symbol = node.left else - path + next end - end - def optional_format?(path, format) - format != false && !path.include?(':format') && !path.end_with?('/') - end + if symbol + symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/ + end + } + + pattern + end + private :build_path - def normalize_options!(options, formatted, path_params, path_ast, modyoule) + private + def add_wildcard_options(options, formatted, path_ast) # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default if formatted != false - path_ast.grep(Journey::Nodes::Star) do |node| - options[node.name.to_sym] ||= /.+?/ - end + path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash| + hash[node.name.to_sym] ||= /.+?/ + }.merge options + else + options end + end + def normalize_options!(options, path_params, modyoule) if path_params.include?(:controller) raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule @@ -178,74 +252,54 @@ module ActionDispatch end def split_constraints(path_params, constraints) - constraints.each_pair do |key, requirement| - if path_params.include?(key) || key == :controller - verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) - @requirements[key] = requirement - else - @conditions[key] = requirement - end - end - end - - def normalize_format!(formatted) - if formatted == true - @requirements[:format] ||= /.+/ - elsif Regexp === formatted - @requirements[:format] = formatted - @defaults[:format] = nil - elsif String === formatted - @requirements[:format] = Regexp.compile(formatted) - @defaults[:format] = formatted + constraints.partition do |key, requirement| + path_params.include?(key) || key == :controller end end - def verify_regexp_requirement(requirement) - if requirement.source =~ ANCHOR_CHARACTERS_REGEX - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - - if requirement.multiline? - raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + def normalize_format(formatted) + case formatted + when true + { requirements: { format: /.+/ }, + defaults: {} } + when Regexp + { requirements: { format: formatted }, + defaults: { format: nil } } + when String + { requirements: { format: Regexp.compile(formatted) }, + defaults: { format: formatted } } + else + { requirements: {}, defaults: {} } end end - def normalize_defaults!(options) - options.each_pair do |key, default| - unless Regexp === default - @defaults[key] = default + def verify_regexp_requirements(requirements) + requirements.each do |requirement| + if requirement.source =~ ANCHOR_CHARACTERS_REGEX + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end - end - end - def verify_callable_constraint(callable_constraint) - unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) - raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" + if requirement.multiline? + raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}" + end end end - def add_request_method(via, conditions) - return if via == [:all] - - if via.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 both GET and POST, add `via: [:get, :post]` option.\n" \ - "If you want to expose your action to GET, use `get` in the router:\n" \ - " Instead of: match \"controller#action\"\n" \ - " Do: get \"controller#action\"" - raise ArgumentError, msg - end - - conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase } + def normalize_defaults(options) + Hash[options.reject { |_, default| Regexp === default }] end def app(blocks) - if to.respond_to?(:call) - Constraints.new(to, blocks, false) - elsif blocks.any? - Constraints.new(dispatcher(defaults), blocks, true) + if to.is_a?(Class) && to < ActionController::Metal + Routing::RouteSet::StaticDispatcher.new to else - dispatcher(defaults) + if to.respond_to?(:call) + Constraints.new(to, blocks, Constraints::CALL) + elsif blocks.any? + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) + else + dispatcher(defaults.key?(:controller)) + end end end @@ -278,7 +332,7 @@ module ActionDispatch def split_to(to) if to =~ /#/ - to.split('#') + to.split("#") else [] end @@ -303,40 +357,29 @@ module ActionDispatch yield end - def blocks(options_constraints, scope_blocks) - if options_constraints && !options_constraints.is_a?(Hash) - verify_callable_constraint(options_constraints) - [options_constraints] - else - scope_blocks || [] + def blocks(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" end + [callable_constraint] end def constraints(options, path_params) - constraints = {} - required_defaults = [] - options.each_pair do |key, option| + options.group_by do |key, option| if Regexp === option - constraints[key] = option + :constraints else - required_defaults << key unless path_params.include?(key) + if path_params.include?(key) + :path_params + else + :required_defaults + end end end - @conditions[:required_defaults] = required_defaults - constraints - end - - def path_params(ast) - ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym } - end - - def path_ast(path) - parser = Journey::Parser.new - parser.parse path end - def dispatcher(defaults) - @set.dispatcher defaults + def dispatcher(raise_on_name_error) + Routing::RouteSet::Dispatcher.new raise_on_name_error end end @@ -354,23 +397,6 @@ module ActionDispatch end module Base - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # 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, :via => :get }.merge!(options) - end - # Matches a url pattern to one or more routes. # # You should not use the +match+ method in your router @@ -443,6 +469,21 @@ module ActionDispatch # dynamic segment used to generate the routes). # You can access that segment from your controller using # <tt>params[<:param>]</tt>. + # In your router: + # + # resources :user, param: :name + # + # You can override <tt>ActiveRecord::Base#to_param</tt> of a related + # model to construct a URL: + # + # class User < ActiveRecord::Base + # def to_param + # name + # end + # end + # + # user = User.find_by(name: 'Phusion') + # user_path(user) # => "/users/Phusion" # # [:path] # The path prefix for the routes. @@ -526,7 +567,7 @@ module ActionDispatch # [:format] # Allows you to specify the default value for optional +format+ # segment or disable it by supplying +false+. - def match(path, options=nil) + def match(path, options = nil) end # Mount a Rack-based application to be used within the application. @@ -551,17 +592,20 @@ module ActionDispatch def mount(app, options = nil) if options path = options.delete(:at) - else - unless Hash === app - raise ArgumentError, "must be called with mount point" - end - + elsif Hash === app options = app app, path = options.find { |k, _| k.respond_to?(:call) } options.delete(app) if app end - raise "A rack application must be specified" unless path + raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) + raise ArgumentError, <<-MSG.strip_heredoc unless path + Must be called with mount point + + mount SomeRackApp, at: "some_route" + or + mount(SomeRackApp => "some_route") + MSG rails_app = rails_app? app options[:as] ||= app_name(app, rails_app) @@ -569,7 +613,7 @@ module ActionDispatch target_as = name_for_action(options[:as], path) options[:via] ||= :all - match(path, options.merge(:to => app, :anchor => false, :format => false)) + match(path, options.merge(to: app, anchor: false, format: false)) define_generate_prefix(app, target_as) if rails_app self @@ -588,7 +632,7 @@ module ActionDispatch # Query if the following named route was already defined. def has_named_route?(name) - @set.named_routes.routes[name.to_sym] + @set.named_routes.key? name end private @@ -616,6 +660,7 @@ module ActionDispatch super(options) else prefix_options = options.slice(*_route.segment_keys) + prefix_options[:relative_url_root] = "".freeze # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } _routes.url_helpers.send("#{name}_path", prefix_options) @@ -764,7 +809,7 @@ module ActionDispatch options = args.extract_options!.dup scope = {} - options[:path] = args.flatten.join('/') if args.any? + options[:path] = args.flatten.join("/") if args.any? options[:constraints] ||= {} unless nested_scope? @@ -773,25 +818,34 @@ module ActionDispatch end if options[:constraints].is_a?(Hash) - defaults = options[:constraints].select do - |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) + defaults = options[:constraints].select do |k, v| + URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer)) end - (options[:defaults] ||= {}).reverse_merge!(defaults) + options[:defaults] = defaults.merge(options[:defaults] || {}) else block, options[:constraints] = options[:constraints], {} end + if options.key?(:only) || options.key?(:except) + scope[:action_options] = { only: options.delete(:only), + except: options.delete(:except) } + end + + if options.key? :anchor + raise ArgumentError, "anchor is ignored unless passed to `match`" + end + @scope.options.each do |option| if option == :blocks value = block elsif option == :options value = options else - value = options.delete(option) + value = options.delete(option) { POISON } end - if value + unless POISON == value scope[option] = send("merge_#{option}_scope", @scope[option], value) end end @@ -803,14 +857,18 @@ module ActionDispatch @scope = @scope.parent end + POISON = Object.new # :nodoc: + # Scopes routes to a specific controller # # controller "food" do # match "bacon", action: :bacon, via: :get # end - def controller(controller, options={}) - options[:controller] = controller - scope(options) { yield } + def controller(controller) + @scope = @scope.new(controller: controller) + yield + ensure + @scope = @scope.parent end # Scopes routes to a specific namespace. For example: @@ -856,13 +914,14 @@ module ActionDispatch defaults = { module: path, - path: options.fetch(:path, path), as: options.fetch(:as, path), shallow_path: options.fetch(:path, path), shallow_prefix: options.fetch(:as, path) } - scope(defaults.merge!(options)) { yield } + path_scope(options.delete(:path) { path }) do + scope(defaults.merge!(options)) { yield } + end end # === Parameter Restriction @@ -921,7 +980,7 @@ module ActionDispatch # resources :iphones # end def constraints(constraints = {}) - scope(:constraints => constraints) { yield } + scope(constraints: constraints) { yield } end # Allows you to set default parameters for a route, such as this: @@ -930,7 +989,10 @@ module ActionDispatch # end # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) - scope(:defaults => defaults) { yield } + @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults)) + yield + ensure + @scope = @scope.parent end private @@ -962,6 +1024,14 @@ module ActionDispatch child end + def merge_via_scope(parent, child) #:nodoc: + child + end + + def merge_format_scope(parent, child) #:nodoc: + child + end + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end @@ -981,15 +1051,15 @@ module ActionDispatch end def merge_options_scope(parent, child) #:nodoc: - (parent || {}).except(*override_keys(child)).merge!(child) + (parent || {}).merge(child) end def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - def override_keys(child) #:nodoc: - child.key?(:only) || child.key?(:except) ? [:only, :except] : [] + def merge_to_scope(parent, child) + child end end @@ -1040,27 +1110,34 @@ module ActionDispatch CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: - attr_reader :controller, :path, :options, :param + attr_reader :controller, :path, :param - def initialize(entities, options = {}) + def initialize(entities, api_only, shallow, 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 - @shallow = false + @shallow = shallow + @api_only = api_only + @only = options.delete :only + @except = options.delete :except end def default_actions - [:index, :create, :new, :show, :update, :destroy, :edit] + if @api_only + [:index, :create, :show, :update, :destroy] + else + [:index, :create, :new, :show, :update, :destroy, :edit] + end end def actions - if only = @options[:only] - Array(only).map(&:to_sym) - elsif except = @options[:except] - default_actions - Array(except).map(&:to_sym) + if @only + Array(@only).map(&:to_sym) + elsif @except + default_actions - Array(@except).map(&:to_sym) else default_actions end @@ -1087,7 +1164,7 @@ module ActionDispatch end def resource_scope - { :controller => controller } + controller end alias :collection_scope :path @@ -1110,17 +1187,15 @@ module ActionDispatch "#{path}/:#{nested_param}" end - def shallow=(value) - @shallow = value - end - def shallow? @shallow end + + def singleton?; false; end end class SingletonResource < Resource #:nodoc: - def initialize(entities, options) + def initialize(entities, api_only, shallow, options) super @as = nil @controller = (options[:controller] || plural).to_s @@ -1128,7 +1203,11 @@ module ActionDispatch end def default_actions - [:show, :create, :update, :destroy, :new, :edit] + if @api_only + [:show, :create, :update, :destroy] + else + [:show, :create, :update, :destroy, :new, :edit] + end end def plural @@ -1144,6 +1223,8 @@ module ActionDispatch alias :member_scope :path alias :nested_scope :path + + def singleton?; true; end end def resources_path_names(options) @@ -1163,11 +1244,11 @@ module ActionDispatch # the plural): # # GET /profile/new - # POST /profile # GET /profile # GET /profile/edit # PATCH/PUT /profile # DELETE /profile + # POST /profile # # === Options # Takes same options as +resources+. @@ -1178,20 +1259,23 @@ module ActionDispatch return self end - resource_scope(:resource, SingletonResource.new(resources.pop, options)) do - yield if block_given? + with_scope_level(:resource) do + options = apply_action_options options + resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - post :create - end if parent_resource.actions.include?(:create) + new do + get :new + end if parent_resource.actions.include?(:new) - new do - get :new - end if parent_resource.actions.include?(:new) + set_member_mappings_for_resource - set_member_mappings_for_resource + collection do + post :create + end if parent_resource.actions.include?(:create) + end end self @@ -1336,21 +1420,24 @@ module ActionDispatch return self end - resource_scope(:resources, Resource.new(resources.pop, options)) do - yield if block_given? + with_scope_level(:resources) do + options = apply_action_options options + resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do + yield if block_given? - concerns(options[:concerns]) if options[:concerns] + concerns(options[:concerns]) if options[:concerns] - collection do - get :index if parent_resource.actions.include?(:index) - post :create if parent_resource.actions.include?(:create) - end + collection do + get :index if parent_resource.actions.include?(:index) + post :create if parent_resource.actions.include?(:create) + end - new do - get :new - end if parent_resource.actions.include?(:new) + new do + get :new + end if parent_resource.actions.include?(:new) - set_member_mappings_for_resource + set_member_mappings_for_resource + end end self @@ -1374,7 +1461,7 @@ module ActionDispatch end with_scope_level(:collection) do - scope(parent_resource.collection_scope) do + path_scope(parent_resource.collection_scope) do yield end end @@ -1398,9 +1485,11 @@ module ActionDispatch with_scope_level(:member) do if shallow? - shallow_scope(parent_resource.member_scope) { yield } + shallow_scope { + path_scope(parent_resource.member_scope) { yield } + } else - scope(parent_resource.member_scope) { yield } + path_scope(parent_resource.member_scope) { yield } end end end @@ -1411,7 +1500,7 @@ module ActionDispatch end with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do + path_scope(parent_resource.new_scope(action_path(:new))) do yield end end @@ -1424,9 +1513,15 @@ module ActionDispatch with_scope_level(:nested) do if shallow? && shallow_nesting_depth >= 1 - shallow_scope(parent_resource.nested_scope, nested_options) { yield } + shallow_scope do + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end + end else - scope(parent_resource.nested_scope, nested_options) { yield } + path_scope(parent_resource.nested_scope) do + scope(nested_options) { yield } + end end end end @@ -1441,13 +1536,14 @@ module ActionDispatch end def shallow - scope(:shallow => true) do - yield - end + @scope = @scope.new(shallow: true) + yield + ensure + @scope = @scope.parent end def shallow? - parent_resource.instance_of?(Resource) && @scope[:shallow] + !parent_resource.singleton? && @scope[:shallow] end # Matches a url pattern to one or more routes. @@ -1456,11 +1552,13 @@ module ActionDispatch # match 'path' => 'controller#action', via: patch # match 'path', to: 'controller#action', via: :post # match 'path', 'otherpath', on: :member, via: :get - def match(path, *rest) + def match(path, *rest, &block) if rest.empty? && Hash === path options = path path, to = options.find { |name, _value| name.is_a?(String) } + raise ArgumentError, "Route path not specified" if path.nil? + case to when Symbol options[:action] = to @@ -1481,77 +1579,30 @@ module ActionDispatch paths = [path] + rest end - 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[:controller] && @scope[:action] - options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" - end - - paths.each do |_path| - route_options = options.dup - route_options[:path] ||= _path if _path.is_a?(String) - - path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') - if using_match_shorthand?(path_without_format, route_options) - route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') - route_options[:to].tr!("-", "_") - end - - decomposed_match(_path, route_options) - end - self - end - - def using_match_shorthand?(path, options) - path && (options[:to] || options[:action]).nil? && path =~ %r{^/?[-\w]+/[-\w/]+$} - end - - def decomposed_match(path, options) # :nodoc: - if on = options.delete(:on) - send(on) { decomposed_match(path, options) } + if options.key?(:defaults) + defaults(options.delete(:defaults)) { map_match(paths, options, &block) } 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 + map_match(paths, options, &block) end end - def add_route(action, options) # :nodoc: - path = path_for_action(action, options.delete(:path)) - raise ArgumentError, "path is required" if path.blank? - - action = action.to_s.dup - - if action =~ /^[\w\-\/]+$/ - options[:action] ||= action.tr('-', '_') unless action.include?("/") - else - action = nil - end - - as = if !options.fetch(:as, true) # if it's set to nil or false - options.delete(:as) - else - name_for_action(options.delete(:as), action) - end - - mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) - app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) - end - - def root(path, options={}) + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # 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(path, options = {}) if path.is_a?(String) options[:to] = path - elsif path.is_a?(Hash) and options.empty? + elsif path.is_a?(Hash) && options.empty? options = path else raise ArgumentError, "must be called with a path and/or options" @@ -1559,12 +1610,12 @@ module ActionDispatch if @scope.resources? with_scope_level(:root) do - scope(parent_resource.path) do - super(options) + path_scope(parent_resource.path) do + match_root_route(options) end end else - super(options) + match_root_route(options) end end @@ -1604,23 +1655,20 @@ module ActionDispatch return true end - unless action_options?(options) - options.merge!(scope_action_options) if scope_action_options? - end - false end - def action_options?(options) #:nodoc: - options[:only] || options[:except] + def apply_action_options(options) # :nodoc: + return options if action_options? options + options.merge scope_action_options end - def scope_action_options? #:nodoc: - @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) + def action_options?(options) #:nodoc: + options[:only] || options[:except] end def scope_action_options #:nodoc: - @scope[:options].slice(:only, :except) + @scope[:action_options] || {} end def resource_scope? #:nodoc: @@ -1635,18 +1683,6 @@ module ActionDispatch @scope.nested? end - def with_exclusive_scope - begin - @scope = @scope.new(:as => nil, :path => nil) - - with_scope_level(:exclusive) do - yield - end - ensure - @scope = @scope.parent - end - end - def with_scope_level(kind) @scope = @scope.new_level(kind) yield @@ -1654,21 +1690,16 @@ module ActionDispatch @scope = @scope.parent end - def resource_scope(kind, resource) #:nodoc: - resource.shallow = @scope[:shallow] - @scope = @scope.new(:scope_level_resource => resource) - @nesting.push(resource) + def resource_scope(resource) #:nodoc: + @scope = @scope.new(scope_level_resource: resource) - with_scope_level(kind) do - scope(parent_resource.resource_scope) { yield } - end + controller(resource.resource_scope) { yield } ensure - @nesting.pop @scope = @scope.parent end def nested_options #:nodoc: - options = { :as => parent_resource.member_name } + options = { as: parent_resource.member_name } options[:constraints] = { parent_resource.nested_param => param_constraint } if param_constraint? @@ -1676,12 +1707,10 @@ module ActionDispatch options end - def nesting_depth #:nodoc: - @nesting.size - end - def shallow_nesting_depth #:nodoc: - @nesting.count(&:shallow?) + @scope.find_all { |node| + node.frame[:scope_level_resource] + }.count { |node| node.frame[:scope_level_resource].shallow? } end def param_constraint? #:nodoc: @@ -1696,27 +1725,28 @@ module ActionDispatch resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope(path, options = {}) #:nodoc: - scope = { :as => @scope[:shallow_prefix], - :path => @scope[:shallow_path] } + def shallow_scope #:nodoc: + scope = { as: @scope[:shallow_prefix], + path: @scope[:shallow_path] } @scope = @scope.new scope - scope(path, options) { yield } + yield ensure @scope = @scope.parent end def path_for_action(action, path) #:nodoc: - if path.blank? && canonical_action?(action) + return "#{@scope[:path]}/#{path}" if path + + if canonical_action?(action) @scope[:path].to_s else - "#{@scope[:path]}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action)}" end end - def action_path(name, path = nil) #:nodoc: - name = name.to_sym if name.is_a?(String) - path || @scope[:path_names][name] || name.to_s + def action_path(name) #:nodoc: + @scope[:path_names][name.to_sym] || name end def prefix_name_for_action(as, action) #:nodoc: @@ -1726,8 +1756,8 @@ module ActionDispatch prefix = action end - if prefix && prefix != '/' && !prefix.empty? - Mapper.normalize_name prefix.to_s.tr('-', '_') + if prefix && prefix != "/" && !prefix.empty? + Mapper.normalize_name prefix.to_s.tr("-", "_") end end @@ -1743,14 +1773,14 @@ module ActionDispatch end action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name) - candidate = action_name.select(&:present?).join('_') + candidate = action_name.select(&:present?).join("_") unless candidate.empty? # 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 candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate) + candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate) else candidate end @@ -1768,6 +1798,124 @@ module ActionDispatch delete :destroy if parent_resource.actions.include?(:destroy) end end + + def api_only? + @set.api_only? + end + + private + + def path_scope(path) + @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) + yield + ensure + @scope = @scope.parent + end + + def map_match(paths, options) + if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) + raise ArgumentError, "Unknown scope #{on.inspect} given to :on" + end + + if @scope[:to] + options[:to] ||= @scope[:to] + end + + if @scope[:controller] && @scope[:action] + options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" + end + + controller = options.delete(:controller) || @scope[:controller] + option_path = options.delete :path + to = options.delete :to + via = Mapping.check_via Array(options.delete(:via) { + @scope[:via] + }) + formatted = options.delete(:format) { @scope[:format] } + anchor = options.delete(:anchor) { true } + options_constraints = options.delete(:constraints) || {} + + path_types = paths.group_by(&:class) + path_types.fetch(String, []).each do |_path| + route_options = options.dup + if _path && option_path + raise ArgumentError, "Ambigous route definition. Both :path and the route path where specified as strings." + end + to = get_to_from_path(_path, to, route_options[:action]) + decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints) + end + + path_types.fetch(Symbol, []).each do |action| + route_options = options.dup + decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints) + end + + self + end + + def get_to_from_path(path, to, action) + return to if to || action + + path_without_format = path.sub(/\(\.:format\)$/, "") + if using_match_shorthand?(path_without_format) + path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_") + else + nil + end + end + + def using_match_shorthand?(path) + path =~ %r{^/?[-\w]+/[-\w/]+$} + end + + def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } + else + case @scope.scope_level + when :resources + nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } + when :resource + member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) } + else + add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints) + end + end + end + + def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc: + path = path_for_action(action, _path) + raise ArgumentError, "path is required" if path.blank? + + action = action.to_s + + default_action = options.delete(:action) || @scope[:action] + + if action =~ /^[\w\-\/]+$/ + default_action ||= action.tr("-", "_") unless action.include?("/") + else + action = nil + end + + as = if !options.fetch(:as, true) # if it's set to nil or false + options.delete(:as) + else + name_for_action(options.delete(:as), action) + end + + path = Mapping.normalize_path URI.parser.escape(path), formatted + ast = Journey::Parser.parse path + + mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options) + @set.add_route(mapping, ast, as, anchor) + end + + def match_root_route(options) + name = has_named_route?(name_for_action(:root, nil)) ? nil : :root + args = ["/", { as: name, via: :get }.merge!(options)] + + match(*args) + end end # Routing Concerns allow you to declare common routes that can be reused @@ -1878,14 +2026,14 @@ module ActionDispatch class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] + :shallow, :blocks, :defaults, :via, :format, :options, :to] RESOURCE_SCOPES = [:resource, :resources] RESOURCE_METHOD_SCOPES = [:collection, :member, :new] attr_reader :parent, :scope_level - def initialize(hash, parent = {}, scope_level = nil) + def initialize(hash, parent = NULL, scope_level = nil) @hash = hash @parent = parent @scope_level = scope_level @@ -1933,27 +2081,33 @@ module ActionDispatch end def new_level(level) - self.class.new(self, self, level) - end - - def fetch(key, &block) - @hash.fetch(key, &block) + self.class.new(frame, self, level) end def [](key) - @hash.fetch(key) { @parent[key] } + scope = find { |node| node.frame.key? key } + scope && scope.frame[key] end - def []=(k,v) - @hash[k] = v + include Enumerable + + def each + node = self + until node.equal? NULL + yield node + node = node.parent + end end + + def frame; @hash; end + + NULL = Scope.new(nil, nil) end def initialize(set) #:nodoc: @set = set - @scope = Scope.new({ :path_names => @set.resources_path_names }) + @scope = Scope.new(path_names: @set.resources_path_names) @concerns = {} - @nesting = [] end include Base diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 9934f5547a..432b9bf4c1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -4,7 +4,7 @@ module ActionDispatch # given an Active Record model instance. They are to be used in combination with # ActionController::Resources. # - # These methods are useful when you want to generate correct URL or path to a RESTful + # These methods are useful when you want to generate the correct URL or path to a RESTful # resource without having to know the exact type of the record in question. # # Nested resources and/or namespaces are also supported, as illustrated in the example: @@ -79,7 +79,7 @@ module ActionDispatch # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app") # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor" # - # For all of these options, see the documentation for <tt>url_for</tt>. + # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor]. # # ==== Functionality # @@ -134,7 +134,6 @@ module ActionDispatch opts end - %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 def #{action}_polymorphic_url(record_or_hash, options = {}) @@ -149,176 +148,175 @@ module ActionDispatch private - def polymorphic_url_for_action(action, record_or_hash, options) - polymorphic_url(record_or_hash, options.merge(:action => action)) - end + def polymorphic_url_for_action(action, record_or_hash, options) + polymorphic_url(record_or_hash, options.merge(action: action)) + end - def polymorphic_path_for_action(action, record_or_hash, options) - polymorphic_path(record_or_hash, options.merge(:action => action)) - end + def polymorphic_path_for_action(action, record_or_hash, options) + polymorphic_path(record_or_hash, options.merge(action: action)) + end - class HelperMethodBuilder # :nodoc: - CACHE = { 'path' => {}, 'url' => {} } + class HelperMethodBuilder # :nodoc: + CACHE = { "path" => {}, "url" => {} } - def self.get(action, type) - type = type.to_s - CACHE[type].fetch(action) { build action, type } - end + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } + end - def self.url; CACHE['url'.freeze][nil]; end - def self.path; CACHE['path'.freeze][nil]; end + def self.url; CACHE["url".freeze][nil]; end + def self.path; CACHE["path".freeze][nil]; end - def self.build(action, type) - prefix = action ? "#{action}_" : "" - suffix = type - if action.to_s == 'new' - HelperMethodBuilder.singular prefix, suffix - else - HelperMethodBuilder.plural prefix, suffix + def self.build(action, type) + prefix = action ? "#{action}_" : "" + suffix = type + if action.to_s == "new" + HelperMethodBuilder.singular prefix, suffix + else + HelperMethodBuilder.plural prefix, suffix + end end - end - def self.singular(prefix, suffix) - new(->(name) { name.singular_route_key }, prefix, suffix) - end + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) + end - def self.plural(prefix, suffix) - new(->(name) { name.route_key }, prefix, suffix) - end + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) + end - def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) - builder = get action, type + def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) + builder = get action, type + + case record_or_hash_or_array + when Array + record_or_hash_or_array = record_or_hash_or_array.compact + if record_or_hash_or_array.empty? + raise ArgumentError, "Nil location provided. Can't build URI." + end + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + recipient = record_or_hash_or_array.shift + end + + method, args = builder.handle_list record_or_hash_or_array + when String, Symbol + method, args = builder.handle_string record_or_hash_or_array + when Class + method, args = builder.handle_class record_or_hash_or_array - case record_or_hash_or_array - when Array - record_or_hash_or_array = record_or_hash_or_array.compact - if record_or_hash_or_array.empty? + when nil raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array end - if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) - recipient = record_or_hash_or_array.shift - end - - method, args = builder.handle_list record_or_hash_or_array - when String, Symbol - method, args = builder.handle_string record_or_hash_or_array - when Class - method, args = builder.handle_class record_or_hash_or_array - when nil - raise ArgumentError, "Nil location provided. Can't build URI." - else - method, args = builder.handle_model record_or_hash_or_array + if options.empty? + recipient.send(method, *args) + else + recipient.send(method, *args, options) + end end + attr_reader :suffix, :prefix - if options.empty? - recipient.send(method, *args) - else - recipient.send(method, *args, options) + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix end - end - - attr_reader :suffix, :prefix - - def initialize(key_strategy, prefix, suffix) - @key_strategy = key_strategy - @prefix = prefix - @suffix = suffix - end - - def handle_string(record) - [get_method_for_string(record), []] - end - - def handle_string_call(target, str) - target.send get_method_for_string str - end - def handle_class(klass) - [get_method_for_class(klass), []] - end + def handle_string(record) + [get_method_for_string(record), []] + end - def handle_class_call(target, klass) - target.send get_method_for_class klass - end + def handle_string_call(target, str) + target.send get_method_for_string str + end - def handle_model(record) - args = [] + def handle_class(klass) + [get_method_for_class(klass), []] + end - model = record.to_model - named_route = if model.persisted? - args << model - get_method_for_string model.model_name.singular_route_key - else - get_method_for_class model - end + def handle_class_call(target, klass) + target.send get_method_for_class klass + end - [named_route, args] - end + def handle_model(record) + args = [] - def handle_model_call(target, model) - method, args = handle_model model - target.send(method, *args) - end + model = record.to_model + named_route = if model.persisted? + args << model + get_method_for_string model.model_name.singular_route_key + else + get_method_for_class model + end - def handle_list(list) - record_list = list.dup - record = record_list.pop + [named_route, args] + end - args = [] + def handle_model_call(target, model) + method, args = handle_model model + target.send(method, *args) + end - route = record_list.map { |parent| - case parent + def handle_list(list) + record_list = list.dup + record = record_list.pop + + args = [] + + route = record_list.map { |parent| + case parent + when Symbol, String + parent.to_s + when Class + args << parent + parent.model_name.singular_route_key + else + args << parent.to_model + parent.to_model.model_name.singular_route_key + end + } + + route << + case record when Symbol, String - parent.to_s + record.to_s when Class - args << parent - parent.model_name.singular_route_key + @key_strategy.call record.model_name else - args << parent.to_model - parent.to_model.model_name.singular_route_key + model = record.to_model + if model.persisted? + args << model + model.model_name.singular_route_key + else + @key_strategy.call model.model_name + end end - } - - route << - case record - when Symbol, String - record.to_s - when Class - @key_strategy.call record.model_name - else - model = record.to_model - if model.persisted? - args << model - model.model_name.singular_route_key - else - @key_strategy.call model.model_name - end - end - route << suffix + route << suffix - named_route = prefix + route.join("_") - [named_route, args] - end + named_route = prefix + route.join("_") + [named_route, args] + end - private + private - def get_method_for_class(klass) - name = @key_strategy.call klass.model_name - get_method_for_string name - end + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + get_method_for_string name + end - def get_method_for_string(str) - "#{prefix}#{str}_#{suffix}" - end + def get_method_for_string(str) + "#{prefix}#{str}_#{suffix}" + end - [nil, 'new', 'edit'].each do |action| - CACHE['url'][action] = build action, 'url' - CACHE['path'][action] = build action, 'path' + [nil, "new", "edit"].each do |action| + CACHE["url"][action] = build action, "url" + CACHE["path"][action] = build action, "path" + end end - end end end end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 3c1c4fadf6..4e2318a45e 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -1,9 +1,9 @@ -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' -require 'action_dispatch/routing/endpoint' +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" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing @@ -22,9 +22,8 @@ module ActionDispatch end def serve(req) - req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) - + unless uri.host if relative_path?(uri.path) uri.path = "#{req.script_name}/#{uri.path}" @@ -32,7 +31,7 @@ module ActionDispatch uri.path = req.script_name.empty? ? "/" : req.script_name end end - + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -40,9 +39,9 @@ module ActionDispatch body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>) headers = { - 'Location' => uri.to_s, - 'Content-Type' => 'text/html', - 'Content-Length' => body.length.to_s + "Location" => uri.to_s, + "Content-Type" => "text/html", + "Content-Length" => body.length.to_s } [ status, headers, [body] ] @@ -58,19 +57,19 @@ module ActionDispatch private def relative_path?(path) - path && !path.empty? && path[0] != '/' + path && !path.empty? && path[0] != "/" end def escape(params) - Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }] end def escape_fragment(params) - Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }] + Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }] end def escape_path(params) - Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }] + Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }] end end @@ -104,11 +103,11 @@ module ActionDispatch def path(params, request) url_options = { - :protocol => request.protocol, - :host => request.host, - :port => request.optional_port, - :path => request.path, - :params => request.query_parameters + 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*\}/) @@ -124,17 +123,16 @@ module ActionDispatch url_options[:script_name] = request.script_name end end - + ActionDispatch::Http::URL.url_for url_options end def inspect - "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})" + "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})" end end module Redirection - # Redirect any path to another path: # # get "/stories" => redirect("/posts") diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index d0d8ded515..5853adb110 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,14 +1,11 @@ -require 'action_dispatch/journey' -require 'forwardable' -require 'thread_safe' -require 'active_support/concern' -require 'active_support/core_ext/object/to_query' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/array/extract_options' -require 'action_controller/metal/exceptions' -require 'action_dispatch/http/request' -require 'action_dispatch/routing/endpoint' +require "action_dispatch/journey" +require "active_support/core_ext/object/to_query" +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/module/remove_method" +require "active_support/core_ext/array/extract_options" +require "action_controller/metal/exceptions" +require "action_dispatch/http/request" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing @@ -21,65 +18,47 @@ module ActionDispatch alias inspect to_s class Dispatcher < Routing::Endpoint - def initialize(defaults) - @defaults = defaults - @controller_class_names = ThreadSafe::Cache.new + def initialize(raise_on_name_error) + @raise_on_name_error = raise_on_name_error end def dispatcher?; true; end def serve(req) - req.check_path_parameters! - params = req.path_parameters - - prepare_params!(params) - - # Just raise undefined constant errors if a controller was specified as default. - unless controller = controller(params, @defaults.key?(:controller)) - return [404, {'X-Cascade' => 'pass'}, []] + params = req.path_parameters + controller = controller req + res = controller.make_response! req + dispatch(controller, params[:action], req, res) + rescue ActionController::RoutingError + if @raise_on_name_error + raise + else + return [404, { "X-Cascade" => "pass" }, []] end - - dispatch(controller, params[:action], req.env) end - def prepare_params!(params) - normalize_controller!(params) - merge_default_action!(params) - end + private - # If this is a default_controller (i.e. a controller specified by the user) - # we should raise an error in case it's not found, because it usually means - # a user error. However, if the controller was retrieved through a dynamic - # segment, as in :controller(/:action), we should simply return nil and - # delegate the control back to Rack cascade. Besides, if this is not a default - # controller, it means we should respect the @scope[:module] parameter. - def controller(params, default_controller=true) - if params && params.key?(:controller) - controller_param = params[:controller] - controller_reference(controller_param) - end + def controller(req) + req.controller_class rescue NameError => e - raise ActionController::RoutingError, e.message, e.backtrace if default_controller + raise ActionController::RoutingError, e.message, e.backtrace end - private - - def controller_reference(controller_param) - const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller" - ActiveSupport::Dependencies.constantize(const_name) + def dispatch(controller, action, req, res) + controller.dispatch(action, req, res) end + end - def dispatch(controller, action, env) - controller.action(action).call(env) + class StaticDispatcher < Dispatcher + def initialize(controller_class) + super(false) + @controller_class = controller_class end - def normalize_controller!(params) - params[:controller] = params[:controller].underscore if params.key?(:controller) - end + private - def merge_default_action!(params) - params[:action] ||= 'index' - end + def controller(_); @controller_class; end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -88,9 +67,10 @@ module ActionDispatch class NamedRouteCollection include Enumerable attr_reader :routes, :url_helpers_module, :path_helpers_module + private :routes def initialize - @routes = {} + @routes = {} @path_helpers = Set.new @url_helpers = Set.new @url_helpers_module = Module.new @@ -142,6 +122,7 @@ module ActionDispatch end def key?(name) + return unless name routes.key? name.to_sym end @@ -198,40 +179,40 @@ module ActionDispatch private - def optimized_helper(args) - params = parameterize_args(args) { |k| - raise_generation_error(args) - } + def optimized_helper(args) + params = parameterize_args(args) do + raise_generation_error(args) + end - @route.format params - end + @route.format params + end - def optimize_routes_generation?(t) - t.send(:optimize_routes_generation?) - end + def optimize_routes_generation?(t) + t.send(:optimize_routes_generation?) + end - def parameterize_args(args) - params = {} - @arg_size.times { |i| - key = @required_parts[i] - value = args[i].to_param - yield key if value.nil? || value.empty? - params[key] = value - } - params - end + def parameterize_args(args) + params = {} + @arg_size.times { |i| + key = @required_parts[i] + value = args[i].to_param + yield key if value.nil? || value.empty? + params[key] = value + } + params + end - def raise_generation_error(args) - missing_keys = [] - params = parameterize_args(args) { |missing_key| - missing_keys << missing_key - } - constraints = Hash[@route.requirements.merge(params).sort_by{|k,v| k.to_s}] - message = "No route matches #{constraints.inspect}" - message << " missing required keys: #{missing_keys.sort.inspect}" + def raise_generation_error(args) + missing_keys = [] + params = parameterize_args(args) { |missing_key| + missing_keys << missing_key + } + constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }] + message = "No route matches #{constraints.inspect}" + message << ", missing required keys: #{missing_keys.sort.inspect}" - raise ActionController::UrlGenerationError, message - end + raise ActionController::UrlGenerationError, message + end end def initialize(route, options, route_name, url_strategy) @@ -267,9 +248,13 @@ module ActionDispatch path_params -= controller_options.keys path_params -= result.keys end - path_params -= inner_options.keys - path_params.take(args.size).each do |param| - result[param] = args.shift + inner_options.each_key do |key| + path_params.delete(key) + end + + args.each_with_index do |arg, index| + param = path_params[index] + result[param] = arg if param end end @@ -278,29 +263,39 @@ module ActionDispatch end private - # Create a url helper allowing ordered parameters to be associated - # with corresponding dynamic segments, so you can do: - # - # foo_url(bar, baz, bang) - # - # Instead of: - # - # foo_url(bar: bar, baz: baz, bang: bang) - # - # Also allow options hash, so you can do: - # - # foo_url(bar, baz, bang, sort_by: 'baz') - # - def define_url_helper(mod, route, name, opts, route_key, url_strategy) - helper = UrlHelper.create(route, opts, route_key, url_strategy) - mod.module_eval do - define_method(name) do |*args| - options = nil - options = args.pop if args.last.is_a? Hash - helper.call self, args, options + # Create a url helper allowing ordered parameters to be associated + # with corresponding dynamic segments, so you can do: + # + # foo_url(bar, baz, bang) + # + # Instead of: + # + # foo_url(bar: bar, baz: baz, bang: bang) + # + # Also allow options hash, so you can do: + # + # foo_url(bar, baz, bang, sort_by: 'baz') + # + def define_url_helper(mod, route, name, opts, route_key, url_strategy) + helper = UrlHelper.create(route, opts, route_key, url_strategy) + mod.module_eval do + define_method(name) do |*args| + last = args.last + options = \ + case last + when Hash + args.pop + when ActionController::Parameters + if last.permitted? + args.pop.to_h + else + raise ArgumentError, ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE + end + end + helper.call self, args, options + end end end - end end # strategy for building urls to send to the client @@ -315,21 +310,27 @@ module ActionDispatch alias :routes :set def self.default_resources_path_names - { :new => 'new', :edit => 'edit' } + { new: "new", edit: "edit" } end def self.new_with_config(config) + route_set_config = DEFAULT_CONFIG + + # engines apparently don't have this set if config.respond_to? :relative_url_root - new Config.new config.relative_url_root - else - # engines apparently don't have this set - new + route_set_config.relative_url_root = config.relative_url_root + end + + if config.respond_to? :api_only + route_set_config.api_only = config.api_only end + + new route_set_config end - Config = Struct.new :relative_url_root + Config = Struct.new :relative_url_root, :api_only - DEFAULT_CONFIG = Config.new(nil) + DEFAULT_CONFIG = Config.new(nil, false) def initialize(config = DEFAULT_CONFIG) self.named_routes = NamedRouteCollection.new @@ -345,17 +346,26 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set - @formatter = Journey::Formatter.new @set + @formatter = Journey::Formatter.new self end def relative_url_root @config.relative_url_root end + def api_only? + @config.api_only + end + def request_class ActionDispatch::Request end + def make_request(env) + request_class.new env + end + private :make_request + def draw(&block) clear! unless @disable_clear_and_finalize eval_block(block) @@ -372,10 +382,6 @@ module ActionDispatch end 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/" - end mapper = Mapper.new(self) if default_scope mapper.with_default_scope(default_scope, &block) @@ -399,10 +405,6 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def dispatcher(defaults) - Routing::RouteSet::Dispatcher.new(defaults) - end - module MountedHelpers extend ActiveSupport::Concern include UrlFor @@ -498,7 +500,7 @@ module ActionDispatch routes.empty? end - def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + def add_route(mapping, path_ast, name, anchor) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] @@ -509,74 +511,32 @@ module ActionDispatch "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end - path = conditions.delete :path_info - ast = conditions.delete :parsed_path_info - path = build_path(path, ast, requirements, anchor) - conditions = build_conditions(conditions, path.names.map(&:to_sym)) - - route = @set.add_route(app, path, conditions, defaults, name) + route = @set.add_route(name, mapping) named_routes[name] = route if name - route - end - - def build_path(path, ast, requirements, anchor) - strexp = Journey::Router::Strexp.new( - ast, - path, - requirements, - SEPARATORS, - anchor) - - 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, path_values) - conditions = current_conditions.dup - - # 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('|')}$] + if route.segment_keys.include?(:controller) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using a dynamic :controller segment in a route is deprecated and + will be removed in Rails 5.1. + MSG end - conditions.keep_if do |k, _| - k == :action || k == :controller || k == :required_defaults || - request_class.public_method_defined?(k) || path_values.include?(k) + if route.segment_keys.include?(:action) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using a dynamic :action segment in a route is deprecated and + will be removed in Rails 5.1. + MSG end + + route end - private :build_conditions class Generator PARAMETERIZE = lambda do |name, value| if name == :controller value - elsif value.is_a?(Array) - value.map(&:to_param).join('/') - elsif param = value.to_param - param + else + value.to_param end end @@ -584,16 +544,14 @@ module ActionDispatch def initialize(named_route, options, recall, set) @named_route = named_route - @options = options.dup - @recall = recall.dup + @options = options + @recall = recall @set = set - normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - normalize_action! end def controller @@ -607,16 +565,11 @@ module ActionDispatch def use_recall_for(key) if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) if !named_route_exists? || segment_keys.include?(key) - @options[key] = @recall.delete(key) + @options[key] = @recall[key] end end end - # Set 'index' as default action for recall - def normalize_recall! - @recall[:action] ||= 'index' - end - def normalize_options! # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -628,12 +581,12 @@ module ActionDispatch # be "index", not the recalled action of "show". if options[:controller] - options[:action] ||= 'index' + options[:action] ||= "index" options[:controller] = options[:controller].to_s end if options.key?(:action) - options[:action] = (options[:action] || 'index').to_s + options[:action] = (options[:action] || "index").to_s end end @@ -643,8 +596,8 @@ module ActionDispatch # :controller, :action or :id is not found, don't pull any # more keys from the recall. def normalize_controller_action_id! - use_recall_for(:controller) or return - use_recall_for(:action) or return + use_recall_for(:controller) || return + use_recall_for(:action) || return use_recall_for(:id) end @@ -652,7 +605,7 @@ module ActionDispatch # is specified, the controller becomes "foo/baz/bat" def use_relative_controller! if !named_route && different_controller? && !controller.start_with?("/") - old_parts = current_controller.split('/') + old_parts = current_controller.split("/") size = controller.count("/") + 1 parts = old_parts[0...-size] << controller @options[:controller] = parts.join("/") @@ -661,13 +614,12 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! - @options[:controller] = controller.sub(%r{^/}, '') if controller - end - - # Move 'index' action from options to recall - def normalize_action! - if @options[:action] == 'index' - @recall[:action] = @options.delete(:action) + if controller + if controller.start_with?("/".freeze) + @options[:controller] = controller[1..-1] + else + @options[:controller] = controller + end end end @@ -694,11 +646,11 @@ module ActionDispatch # Generate the path indicated by the arguments, and return an array of # the keys that were not used to generate it. - def extra_keys(options, recall={}) + def extra_keys(options, recall = {}) generate_extras(options, recall).last end - def generate_extras(options, recall={}) + def generate_extras(options, recall = {}) route_key = options.delete :use_route path, params = generate(route_key, options, recall) return path, params.keys @@ -711,14 +663,18 @@ module ActionDispatch RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, :trailing_slash, :anchor, :params, :only_path, :script_name, - :original_script_name] + :original_script_name, :relative_url_root] def optimize_routes_generation? default_url_options.empty? end def find_script_name(options) - options.delete(:script_name) || relative_url_root || '' + options.delete(:script_name) || find_relative_url_root(options) || "" + end + + def find_relative_url_root(options) + options.delete(:relative_url_root) || relative_url_root end def path_for(options, route_name = nil) @@ -736,7 +692,7 @@ module ActionDispatch password = options.delete :password end - recall = options.delete(:_recall) { {} } + recall = options.delete(:_recall) { {} } original_script_name = options.delete(:original_script_name) script_name = find_script_name options @@ -764,7 +720,7 @@ module ActionDispatch end def call(env) - req = request_class.new(env) + req = make_request(env) req.path_info = Journey::Router::Utils.normalize_path(req.path_info) @router.serve(req) end @@ -775,12 +731,12 @@ module ActionDispatch extras = environment[:extras] || {} begin - env = Rack::MockRequest.env_for(path, {:method => method}) + env = Rack::MockRequest.env_for(path, method: method) rescue URI::InvalidURIError => e raise ActionController::RoutingError, e.message end - req = request_class.new(env) + req = make_request(env) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -793,14 +749,13 @@ module ActionDispatch req.path_parameters = old_params.merge params app = route.app if app.matches?(req) && app.dispatcher? - dispatcher = app.app - - if dispatcher.controller(params, false) - dispatcher.prepare_params!(params) - return params - else + begin + req.controller_class + rescue NameError raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller" end + + return req.path_parameters end end diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index 040ea04046..ee847eaeed 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/array/extract_options' +require "active_support/core_ext/array/extract_options" module ActionDispatch module Routing @@ -19,7 +19,7 @@ module ActionDispatch end end - def respond_to?(method, include_private = false) + def respond_to_missing?(method, include_private = false) super || @helpers.respond_to?(method) end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index eb554ec383..a1ac5a2b6c 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -1,7 +1,7 @@ module ActionDispatch module Routing # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse - # is also possible: an URL can be generated from one of your routing definitions. + # is also possible: a URL can be generated from one of your routing definitions. # URL generation functionality is centralized in this module. # # See ActionDispatch::Routing for general information about routing and routes.rb. @@ -52,9 +52,11 @@ module ActionDispatch # argument. # # 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 that's why you'll still - # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # 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 you still have to provide + # the +:host+ argument or set the default host that will be used in all mailers using the + # configuration option +config.action_mailer.default_url_options+. For more information on + # url_for in mailers read the ActionMailer#Base documentation. # # # == URL generation for named routes @@ -147,6 +149,20 @@ module ActionDispatch # # => 'http://somehost.org/myapp/tasks/testing' # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true # # => '/myapp/tasks/testing' + # + # Missing routes keys may be filled in from the current request's parameters + # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are + # placed in the path). Given that the current action has been reached + # through `GET /users/1`: + # + # url_for(only_path: true) # => '/users/1' + # url_for(only_path: true, action: 'edit') # => '/users/1/edit' + # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit' + # + # Notice that no +:id+ parameter was provided to the first +url_for+ call + # and the helper used the one from the route's path. Any path parameter + # implicitly used by +url_for+ can always be overwritten like shown on the + # last +url_for+ calls. def url_for(options = nil) case options when nil @@ -155,12 +171,20 @@ module ActionDispatch route_name = options.delete :use_route _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), route_name) + when ActionController::Parameters + unless options.permitted? + raise ArgumentError.new(ActionDispatch::Routing::INSECURE_URL_PARAMETERS_MESSAGE) + end + route_name = options.delete :use_route + _routes.url_for(options.to_h.symbolize_keys. + reverse_merge!(url_options), route_name) when String options when Symbol HelperMethodBuilder.url.handle_string_call self, options when Array - polymorphic_url(options, options.extract_options!) + components = options.dup + polymorphic_url(components, components.extract_options!) when Class HelperMethodBuilder.url.handle_class_call self, options else @@ -170,20 +194,20 @@ module ActionDispatch protected - def optimize_routes_generation? - _routes.optimize_routes_generation? && default_url_options.empty? - end + def optimize_routes_generation? + _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 _with_routes(routes) + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end - def _routes_context - self - end + def _routes_context + self + end end end end |