diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/routing')
-rw-r--r-- | actionpack/lib/action_dispatch/routing/endpoint.rb | 11 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/inspector.rb | 221 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 667 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/polymorphic_routes.rb | 301 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/redirection.rb | 56 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/route_set.rb | 344 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/routes_proxy.rb | 37 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/url_for.rb | 76 |
8 files changed, 1067 insertions, 646 deletions
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb index 88aa13c3e8..28bb20d688 100644 --- a/actionpack/lib/action_dispatch/routing/endpoint.rb +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -1,10 +1,17 @@ +# frozen_string_literal: true + module ActionDispatch module Routing class Endpoint # :nodoc: def dispatcher?; false; end def redirect?; false; end - def matches?(req); true; end - def app; self; end + def matches?(req); true; end + def app; self; end + def rack_app; app; end + + def engine? + rack_app.is_a?(Class) && rack_app < Rails::Engine + end end end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 5d30a545a2..413e524ef6 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -1,5 +1,7 @@ -require 'delegate' -require 'active_support/core_ext/string/strip' +# frozen_string_literal: true + +require "delegate" +require "io/console/size" module ActionDispatch module Routing @@ -13,7 +15,7 @@ module ActionDispatch end def rack_app - app.app + app.rack_app end def path @@ -33,11 +35,11 @@ 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? @@ -45,7 +47,7 @@ module ActionDispatch end def engine? - rack_app.respond_to?(:routes) + app.engine? end end @@ -59,11 +61,11 @@ module ActionDispatch @routes = routes end - def format(formatter, filter = nil) + def format(formatter, filter = {}) routes_to_display = filter_routes(normalize_filter(filter)) routes = collect_routes(routes_to_display) if routes.none? - formatter.no_routes(collect_routes(@routes)) + formatter.no_routes(collect_routes(@routes), filter) return formatter.result end @@ -79,107 +81,156 @@ module ActionDispatch end private - - 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}/ } + def normalize_filter(filter) + if filter[:controller] + { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ } + elsif filter[:grep] + { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/, + verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ } + end 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 } + 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 - else - @routes 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 - def initialize - @buffer = [] - end + module ConsoleFormatter + class Base + def initialize + @buffer = [] + end - def result - @buffer.join("\n") - end + def result + @buffer.join("\n") + end - def section_title(title) - @buffer << "\n#{title}:" - end + def section_title(title) + end - def section(routes) - @buffer << draw_section(routes) - end + def section(routes) + end - def header(routes) - @buffer << draw_header(routes) + def header(routes) + end + + def no_routes(routes, filter) + @buffer << + if routes.none? + <<~MESSAGE + You don't have any routes defined! + + Please add some routes in config/routes.rb. + MESSAGE + elsif filter.key?(:controller) + "No routes were found for this controller." + elsif filter.key?(:grep) + "No routes were found for this grep pattern." + end + + @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html." + end end - def no_routes(routes) - @buffer << - if routes.none? - <<-MESSAGE.strip_heredoc - You don't have any routes defined! + class Sheet < Base + def section_title(title) + @buffer << "\n#{title}:" + end - Please add some routes in config/routes.rb. - MESSAGE - else - "No routes were found for this controller" + def section(routes) + @buffer << draw_section(routes) 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) - name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) + def header(routes) + @buffer << draw_header(routes) + end + + private - routes.map do |r| - "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" + def draw_section(routes) + 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| + "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" + end end - end - def draw_header(routes) - name_width, verb_width, path_width = widths(routes) + def draw_header(routes) + name_width, verb_width, path_width = widths(routes) - "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action" + "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action" + end + + def widths(routes) + [routes.map { |r| r[:name].length }.max || 0, + routes.map { |r| r[:verb].length }.max || 0, + routes.map { |r| r[:path].length }.max || 0] + end + end + + class Expanded < Base + def section_title(title) + @buffer << "\n#{"[ #{title} ]"}" end - def widths(routes) - [routes.map { |r| r[:name].length }.max || 0, - routes.map { |r| r[:verb].length }.max || 0, - routes.map { |r| r[:path].length }.max || 0] + def section(routes) + @buffer << draw_expanded_section(routes) end + + private + + def draw_expanded_section(routes) + routes.map.each_with_index do |r, i| + <<~MESSAGE.chomp + #{route_header(index: i + 1)} + Prefix | #{r[:name]} + Verb | #{r[:verb]} + URI | #{r[:path]} + Controller#Action | #{r[:reqs]} + MESSAGE + end + end + + def route_header(index:) + console_width = IO.console_size.second + header_prefix = "--[ Route #{index} ]" + dash_remainder = [console_width - header_prefix.size, 0].max + + "#{header_prefix}#{'-' * dash_remainder}" + end + end end class HtmlTableFormatter @@ -196,21 +247,21 @@ module ActionDispatch @buffer << @view.render(partial: "routes/route", collection: routes) end - # the header is part of the HTML page, so we don't construct it here. + # The header is part of the HTML page, so we don't construct it here. def header(routes) end def no_routes(*) - @buffer << <<-MESSAGE.strip_heredoc + @buffer << <<~MESSAGE <p>You don't have any routes defined!</p> <ul> <li>Please add some routes in <tt>config/routes.rb</tt>.</li> <li> For more information about routes, please see the Rails guide - <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>. + <a href="https://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>. </li> </ul> - MESSAGE + MESSAGE end def result diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 81ffea4d5d..08560ccf7d 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,10 +1,11 @@ -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/regexp' -require 'action_dispatch/routing/redirection' -require 'action_dispatch/routing/endpoint' +# frozen_string_literal: true + +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/regexp" +require "action_dispatch/routing/redirection" +require "action_dispatch/routing/endpoint" module ActionDispatch module Routing @@ -18,9 +19,9 @@ module ActionDispatch CALL = ->(app, req) { app.call req.env } def initialize(app, constraints, strategy) - # Unwrap Constraints objects. I don't actually think it's possible + # 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 + # multiple places that kept testing children of this object. I # *think* they were just being defensive, but I have no idea. if app.is_a?(self.class) constraints += app.constraints @@ -42,19 +43,32 @@ module ActionDispatch end def serve(req) - return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req) + return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req) @strategy.call @app, req end private def constraint_args(constraint, request) - constraint.arity == 1 ? [request] : [request.path_parameters, request] + arity = if constraint.respond_to?(:arity) + constraint.arity + else + constraint.method(:call).arity + end + + if arity < 1 + [] + elsif arity == 1 + [request] + else + [request.path_parameters, request] + end end end class Mapping #:nodoc: ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z} attr_reader :requirements, :defaults attr_reader :to, :default_controller, :default_action @@ -94,7 +108,7 @@ module ActionDispatch end def self.optional_format?(path, format) - format != false && !path.include?(':format') && !path.end_with?('/') + format != false && path !~ OPTIONAL_FORMAT_REGEX end def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options) @@ -107,7 +121,7 @@ module ActionDispatch @ast = ast @anchor = anchor @via = via - @internal = options[:internal] + @internal = options.delete(:internal) path_params = ast.find_all(&:symbol?).map(&:to_sym) @@ -121,7 +135,7 @@ module ActionDispatch if options_constraints.is_a?(Hash) @defaults = Hash[options_constraints.find_all { |key, default| - URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + URL_OPTIONS.include?(key) && (String === default || Integer === default) }].merge @defaults @blocks = blocks constraints.merge! options_constraints @@ -138,21 +152,16 @@ module ActionDispatch @conditions = Hash[conditions] @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options)) + if path_params.include?(:action) && !@requirements.key?(:action) + @defaults[:action] ||= "index" + end + @required_defaults = (split_options[:required_defaults] || []).map(&:first) end def make_route(name, precedence) - route = Journey::Route.new(name, - application, - path, - conditions, - required_defaults, - defaults, - request_method, - precedence, - @internal) - - route + Journey::Route.new(name, application, path, conditions, required_defaults, + defaults, request_method, precedence, @internal) end def application @@ -215,7 +224,7 @@ module ActionDispatch 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 + # optional format part of the route by default. if formatted != false path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash| hash[node.name.to_sym] ||= /.+?/ @@ -236,7 +245,7 @@ module ActionDispatch options[:controller] ||= /.+?/ end - if to.respond_to? :call + if to.respond_to?(:action) || to.respond_to?(:call) options else to_endpoint = split_to to @@ -267,13 +276,13 @@ module ActionDispatch { requirements: { format: Regexp.compile(formatted) }, defaults: { format: formatted } } else - { requirements: { }, defaults: { } } + { requirements: {}, defaults: {} } end end def verify_regexp_requirements(requirements) requirements.each do |requirement| - if requirement.source =~ ANCHOR_CHARACTERS_REGEX + if ANCHOR_CHARACTERS_REGEX.match?(requirement.source) raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end @@ -288,24 +297,22 @@ module ActionDispatch end def app(blocks) - if to.is_a?(Class) && to < ActionController::Metal + if to.respond_to?(:action) Routing::RouteSet::StaticDispatcher.new to + elsif to.respond_to?(:call) + Constraints.new(to, blocks, Constraints::CALL) + elsif blocks.any? + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) else - 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 + dispatcher(defaults.key?(:controller)) end end def check_controller_and_action(path_params, controller, action) hash = check_part(:controller, controller, path_params, {}) do |part| translate_controller(part) { - message = "'#{part}' is not a supported controller name. This can lead to potential routing problems." - message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" + message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems." + message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" raise ArgumentError, message } @@ -329,8 +336,8 @@ module ActionDispatch end def split_to(to) - if to =~ /#/ - to.split('#') + if /#/.match?(to) + to.split("#") else [] end @@ -338,7 +345,7 @@ module ActionDispatch def add_controller_module(controller, modyoule) if modyoule && !controller.is_a?(Regexp) - if controller =~ %r{\A/} + if %r{\A/}.match?(controller) controller[1..-1] else [modyoule, controller].compact.join("/") @@ -386,7 +393,7 @@ 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{^/(\(+[^)]+\)){1,}$} path end @@ -395,7 +402,7 @@ module ActionDispatch end module Base - # Matches a url pattern to one or more routes. + # Matches a URL pattern to one or more routes. # # You should not use the +match+ method in your router # without specifying an HTTP method. @@ -405,7 +412,7 @@ module ActionDispatch # # sets :controller, :action and :id in params # match ':controller/:action/:id', via: [:get, :post] # - # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # Note that +:controller+, +:action+ and +:id+ are interpreted as URL # query parameters and thus available through +params+ in an action. # # If you want to expose your action to GET, use +get+ in the router: @@ -454,7 +461,7 @@ module ActionDispatch # # === Options # - # Any options not seen here are passed on as params with the url. + # Any options not seen here are passed on as params with the URL. # # [:controller] # The route's controller. @@ -469,7 +476,17 @@ module ActionDispatch # <tt>params[<:param>]</tt>. # In your router: # - # resources :user, param: :name + # resources :users, param: :name + # + # The +users+ resource here will have the following routes generated for it: + # + # GET /users(.:format) + # POST /users(.:format) + # GET /users/new(.:format) + # GET /users/:name/edit(.:format) + # GET /users/:name(.:format) + # PATCH/PUT /users/:name(.:format) + # DELETE /users/:name(.:format) # # You can override <tt>ActiveRecord::Base#to_param</tt> of a related # model to construct a URL: @@ -480,8 +497,8 @@ module ActionDispatch # end # end # - # user = User.find_by(name: 'Phusion') - # user_path(user) # => "/users/Phusion" + # user = User.find_by(name: 'Phusion') + # user_path(user) # => "/users/Phusion" # # [:path] # The path prefix for the routes. @@ -539,10 +556,10 @@ module ActionDispatch # # match 'json_only', constraints: { format: 'json' }, via: :get # - # class Whitelist + # class PermitList # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get + # match 'path', to: 'c#a', constraints: PermitList.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -565,7 +582,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. @@ -597,7 +614,7 @@ module ActionDispatch end raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) - raise ArgumentError, <<-MSG.strip_heredoc unless path + raise ArgumentError, <<~MSG unless path Must be called with mount point mount SomeRackApp, at: "some_route" @@ -611,7 +628,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 @@ -630,7 +647,7 @@ module ActionDispatch # Query if the following named route was already defined. def has_named_route?(name) - @set.named_routes.key? name + @set.named_routes.key?(name) end private @@ -650,18 +667,31 @@ module ActionDispatch def define_generate_prefix(app, name) _route = @set.named_routes.get name _routes = @set - app.routes.define_mounted_helper(name) + _url_helpers = @set.url_helpers + + script_namer = ->(options) do + prefix_options = options.slice(*_route.segment_keys) + prefix_options[:relative_url_root] = "" + + if options[:_recall] + prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys)) + end + + # We must actually delete prefix segment keys to avoid passing them to next url_for. + _route.segment_keys.each { |k| options.delete(k) } + _url_helpers.send("#{name}_path", prefix_options) + end + + app.routes.define_mounted_helper(name, script_namer) + app.routes.extend Module.new { def optimize_routes_generation?; false; end + define_method :find_script_name do |options| if options.key? :script_name 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) + script_namer.call(options) end end } @@ -713,11 +743,7 @@ module ActionDispatch def map_method(method, args, &block) options = args.extract_options! options[:via] = method - if options.key?(:defaults) - defaults(options.delete(:defaults)) { match(*args, options, &block) } - else - match(*args, options, &block) - end + match(*args, options, &block) self end end @@ -811,7 +837,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? @@ -821,10 +847,10 @@ module ActionDispatch 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)) + 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 @@ -835,7 +861,7 @@ module ActionDispatch end if options.key? :anchor - raise ArgumentError, 'anchor is ignored unless passed to `match`' + raise ArgumentError, "anchor is ignored unless passed to `match`" end @scope.options.each do |option| @@ -982,7 +1008,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: @@ -998,67 +1024,71 @@ module ActionDispatch end private - def merge_path_scope(parent, child) #:nodoc: + def merge_path_scope(parent, child) Mapper.normalize_path("#{parent}/#{child}") end - def merge_shallow_path_scope(parent, child) #:nodoc: + def merge_shallow_path_scope(parent, child) Mapper.normalize_path("#{parent}/#{child}") end - def merge_as_scope(parent, child) #:nodoc: + def merge_as_scope(parent, child) parent ? "#{parent}_#{child}" : child end - def merge_shallow_prefix_scope(parent, child) #:nodoc: + def merge_shallow_prefix_scope(parent, child) parent ? "#{parent}_#{child}" : child end - def merge_module_scope(parent, child) #:nodoc: + def merge_module_scope(parent, child) parent ? "#{parent}/#{child}" : child end - def merge_controller_scope(parent, child) #:nodoc: + def merge_controller_scope(parent, child) child end - def merge_action_scope(parent, child) #:nodoc: + def merge_action_scope(parent, child) child end - def merge_via_scope(parent, child) #:nodoc: + def merge_via_scope(parent, child) child end - def merge_format_scope(parent, child) #:nodoc: + def merge_format_scope(parent, child) child end - def merge_path_names_scope(parent, child) #:nodoc: + def merge_path_names_scope(parent, child) merge_options_scope(parent, child) end - def merge_constraints_scope(parent, child) #:nodoc: + def merge_constraints_scope(parent, child) merge_options_scope(parent, child) end - def merge_defaults_scope(parent, child) #:nodoc: + def merge_defaults_scope(parent, child) merge_options_scope(parent, child) end - def merge_blocks_scope(parent, child) #:nodoc: + def merge_blocks_scope(parent, child) merged = parent ? parent.dup : [] merged << child if child merged end - def merge_options_scope(parent, child) #:nodoc: + def merge_options_scope(parent, child) (parent || {}).merge(child) end - def merge_shallow_scope(parent, child) #:nodoc: + def merge_shallow_scope(parent, child) child ? true : false end + + def merge_to_scope(parent, child) + child + end end # Resource routing allows you to quickly declare all of the common routes @@ -1111,6 +1141,10 @@ module ActionDispatch attr_reader :controller, :path, :param def initialize(entities, api_only, shallow, options = {}) + if options[:param].to_s.include?(":") + raise ArgumentError, ":param option can't contain colons" + end + @name = entities.to_s @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @@ -1132,10 +1166,16 @@ module ActionDispatch end def actions + if @except + available_actions - Array(@except).map(&:to_sym) + else + available_actions + end + end + + def available_actions if @only Array(@only).map(&:to_sym) - elsif @except - default_actions - Array(@except).map(&:to_sym) else default_actions end @@ -1237,19 +1277,19 @@ module ActionDispatch # # resource :profile # - # creates six different routes in your application, all mapping to + # This creates six different routes in your application, all mapping to # the +Profiles+ controller (note that the controller is named after # 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+. + # Takes same options as resources[rdoc-ref:#resources] def resource(*resources, &block) options = resources.extract_options!.dup @@ -1264,15 +1304,15 @@ module ActionDispatch 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) set_member_mappings_for_resource + + collection do + post :create + end if parent_resource.actions.include?(:create) end end @@ -1314,7 +1354,7 @@ module ActionDispatch # DELETE /photos/:photo_id/comments/:id # # === Options - # Takes same options as <tt>Base#match</tt> as well as: + # Takes same options as match[rdoc-ref:Base#match] as well as: # # [:path_names] # Allows you to change the segment component of the +edit+ and +new+ actions. @@ -1322,14 +1362,14 @@ module ActionDispatch # # resources :posts, path_names: { new: "brand_new" } # - # The above example will now change /posts/new to /posts/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 + # The resource and all segments will now route to /postings instead of /posts. # # [:only] # Only generate routes for the given actions. @@ -1406,6 +1446,9 @@ module ActionDispatch # Allows you to specify the default value for optional +format+ # segment or disable it by supplying +false+. # + # [:param] + # Allows you to override the default param name of +:id+ in the URL. + # # === Examples # # # routes call <tt>Admin::PostsController</tt> @@ -1526,7 +1569,7 @@ module ActionDispatch end end - # See ActionDispatch::Routing::Mapper::Scoping#namespace + # See ActionDispatch::Routing::Mapper::Scoping#namespace. def namespace(path, options = {}) if resource_scope? nested { super } @@ -1546,22 +1589,24 @@ module ActionDispatch !parent_resource.singleton? && @scope[:shallow] end - # Matches a url pattern to one or more routes. + # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # - # match 'path' => 'controller#action', via: patch + # 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 when String - if to =~ /#/ + if /#/.match?(to) options[:to] = to else options[:controller] = to @@ -1577,110 +1622,13 @@ module ActionDispatch paths = [path] + rest end - 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 - - 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 - ActiveSupport::Deprecation.warn <<-eowarn -Specifying strings for both :path and the route path is deprecated. Change things like this: - - match #{_path.inspect}, :path => #{option_path.inspect} - -to this: - - match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect} - eowarn - route_options[:action] = _path - route_options[:as] = _path - _path = option_path - 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("-", "_") + if options.key?(:defaults) + defaults(options.delete(:defaults)) { map_match(paths, options, &block) } else - nil + map_match(paths, options, &block) 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 - # You can specify what Rails should route "/" to with the root method: # # root to: 'pages#main' @@ -1697,7 +1645,7 @@ to this: 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" @@ -1714,13 +1662,13 @@ to this: end end - protected + private - def parent_resource #:nodoc: + def parent_resource @scope[:scope_level_resource] end - def apply_common_behavior_for(method, resources, options, &block) #:nodoc: + def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true @@ -1754,48 +1702,48 @@ to this: false end - def apply_action_options(options) # :nodoc: + def apply_action_options(options) return options if action_options? options options.merge scope_action_options end - def action_options?(options) #:nodoc: + def action_options?(options) options[:only] || options[:except] end - def scope_action_options #:nodoc: + def scope_action_options @scope[:action_options] || {} end - def resource_scope? #:nodoc: + def resource_scope? @scope.resource_scope? end - def resource_method_scope? #:nodoc: + def resource_method_scope? @scope.resource_method_scope? end - def nested_scope? #:nodoc: + def nested_scope? @scope.nested? end - def with_scope_level(kind) + def with_scope_level(kind) # :doc: @scope = @scope.new_level(kind) yield ensure @scope = @scope.parent end - def resource_scope(resource) #:nodoc: - @scope = @scope.new(:scope_level_resource => resource) + def resource_scope(resource) + @scope = @scope.new(scope_level_resource: resource) controller(resource.resource_scope) { yield } ensure @scope = @scope.parent end - def nested_options #:nodoc: - options = { :as => parent_resource.member_name } + def nested_options + options = { as: parent_resource.member_name } options[:constraints] = { parent_resource.nested_param => param_constraint } if param_constraint? @@ -1803,27 +1751,27 @@ to this: options end - def shallow_nesting_depth #:nodoc: + def shallow_nesting_depth @scope.find_all { |node| node.frame[:scope_level_resource] }.count { |node| node.frame[:scope_level_resource].shallow? } end - def param_constraint? #:nodoc: + def param_constraint? @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) end - def param_constraint #:nodoc: + def param_constraint @scope[:constraints][parent_resource.param] end - def canonical_action?(action) #:nodoc: + def canonical_action?(action) resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scope #:nodoc: - scope = { :as => @scope[:shallow_prefix], - :path => @scope[:shallow_path] } + def shallow_scope + scope = { as: @scope[:shallow_prefix], + path: @scope[:shallow_path] } @scope = @scope.new scope yield @@ -1831,7 +1779,7 @@ to this: @scope = @scope.parent end - def path_for_action(action, path) #:nodoc: + def path_for_action(action, path) return "#{@scope[:path]}/#{path}" if path if canonical_action?(action) @@ -1841,23 +1789,23 @@ to this: end end - def action_path(name) #:nodoc: + def action_path(name) @scope[:path_names][name.to_sym] || name end - def prefix_name_for_action(as, action) #:nodoc: + def prefix_name_for_action(as, action) if as prefix = as elsif !canonical_action?(action) 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 - def name_for_action(as, action) #:nodoc: + def name_for_action(as, action) prefix = prefix_name_for_action(as, action) name_prefix = @scope[:as] @@ -1869,7 +1817,7 @@ to this: 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 @@ -1883,7 +1831,7 @@ to this: end end - def set_member_mappings_for_resource + def set_member_mappings_for_resource # :doc: member do get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) @@ -1895,22 +1843,119 @@ to this: end end - def api_only? + def api_only? # :doc: @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 path_scope(path) + @scope = @scope.new(path: merge_path_scope(@scope[:path], path)) + yield + ensure + @scope = @scope.parent + end - def match_root_route(options) - name = has_named_route?(:root) ? nil : :root - match '/', { :as => name, :via => :get }.merge!(options) - 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, "Ambiguous route definition. Both :path and the route path were 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) + 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) + 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 /^[\w\-\/]+$/.match?(action) + 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, as) + end + + def match_root_route(options) + args = ["/", { as: :root, via: :get }.merge(options)] + match(*args) + end end # Routing Concerns allow you to declare common routes that can be reused @@ -2001,7 +2046,7 @@ to this: # concerns :commentable # end # - # concerns also work in any routes helper that you want to use: + # Concerns also work in any routes helper that you want to use: # # namespace :posts do # concerns :commentable @@ -2018,10 +2063,124 @@ to this: end end + module CustomUrls + # Define custom URL helpers that will be added to the application's + # routes. This allows you to override and/or replace the default behavior + # of routing helpers, e.g: + # + # direct :homepage do + # "http://www.rubyonrails.org" + # end + # + # direct :commentable do |model| + # [ model, anchor: model.dom_id ] + # end + # + # direct :main do + # { controller: "pages", action: "index", subdomain: "www" } + # end + # + # The return value from the block passed to +direct+ must be a valid set of + # arguments for +url_for+ which will actually build the URL string. This can + # be one of the following: + # + # * A string, which is treated as a generated URL + # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt> + # * An array, which is passed to +polymorphic_url+ + # * An Active Model instance + # * An Active Model class + # + # NOTE: Other URL helpers can be called in the block but be careful not to invoke + # your custom URL helper again otherwise it will result in a stack overflow error. + # + # You can also specify default options that will be passed through to + # your URL helper definition, e.g: + # + # direct :browse, page: 1, size: 10 do |options| + # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ] + # end + # + # In this instance the +params+ object comes from the context in which the + # block is executed, e.g. generating a URL inside a controller action or a view. + # If the block is executed where there isn't a +params+ object such as this: + # + # Rails.application.routes.url_helpers.browse_path + # + # then it will raise a +NameError+. Because of this you need to be aware of the + # context in which you will use your custom URL helper when defining it. + # + # NOTE: The +direct+ method can't be used inside of a scope block such as + # +namespace+ or +scope+ and will raise an error if it detects that it is. + def direct(name, options = {}, &block) + unless @scope.root? + raise RuntimeError, "The direct method can't be used inside a routes scope block" + end + + @set.add_url_helper(name, options, &block) + end + + # Define custom polymorphic mappings of models to URLs. This alters the + # behavior of +polymorphic_url+ and consequently the behavior of + # +link_to+ and +form_for+ when passed a model instance, e.g: + # + # resource :basket + # + # resolve "Basket" do + # [:basket] + # end + # + # This will now generate "/basket" when a +Basket+ instance is passed to + # +link_to+ or +form_for+ instead of the standard "/baskets/:id". + # + # NOTE: This custom behavior only applies to simple polymorphic URLs where + # a single model instance is passed and not more complicated forms, e.g: + # + # # config/routes.rb + # resource :profile + # namespace :admin do + # resources :users + # end + # + # resolve("User") { [:profile] } + # + # # app/views/application/_menu.html.erb + # link_to "Profile", @current_user + # link_to "Profile", [:admin, @current_user] + # + # The first +link_to+ will generate "/profile" but the second will generate + # the standard polymorphic URL of "/admin/users/1". + # + # You can pass options to a polymorphic mapping - the arity for the block + # needs to be two as the instance is passed as the first argument, e.g: + # + # resolve "Basket", anchor: "items" do |basket, options| + # [:basket, options] + # end + # + # This generates the URL "/basket#items" because when the last item in an + # array passed to +polymorphic_url+ is a hash then it's treated as options + # to the URL helper that gets called. + # + # NOTE: The +resolve+ method can't be used inside of a scope block such as + # +namespace+ or +scope+ and will raise an error if it detects that it is. + def resolve(*args, &block) + unless @scope.root? + raise RuntimeError, "The resolve method can't be used inside a routes scope block" + end + + options = args.extract_options! + args = args.flatten(1) + + args.each do |klass| + @set.add_polymorphic_mapping(klass, options, &block) + end + end + end + class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :via, :format, :options] + :shallow, :blocks, :defaults, :via, :format, :options, :to] RESOURCE_SCOPES = [:resource, :resources] RESOURCE_METHOD_SCOPES = [:collection, :member, :new] @@ -2038,6 +2197,14 @@ to this: scope_level == :nested end + def null? + @hash.nil? && @parent.nil? + end + + def root? + @parent.null? + end + def resources? scope_level == :resources end @@ -2088,8 +2255,7 @@ to this: def each node = self - loop do - break if node.equal? NULL + until node.equal? NULL yield node node = node.parent end @@ -2102,7 +2268,7 @@ to this: 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 = {} end @@ -2112,6 +2278,7 @@ to this: include Scoping include Concerns include Resources + include CustomUrls end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 9934f5547a..4de5f9e2f7 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActionDispatch module Routing # Polymorphic URL helpers are methods for smart resolution to a named route call when # 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: @@ -40,7 +42,7 @@ module ActionDispatch # # Example usage: # - # edit_polymorphic_path(@post) # => "/posts/1/edit" + # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf" # # == Usage with mounted engines @@ -79,7 +81,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 # @@ -103,6 +105,10 @@ module ActionDispatch return polymorphic_url record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array, options], false) + end + opts = options.dup action = opts.delete :action type = opts.delete(:routing_type) || :url @@ -114,8 +120,7 @@ module ActionDispatch opts end - # Returns the path component of a URL for the given record. It uses - # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. + # Returns the path component of a URL for the given record. def polymorphic_path(record_or_hash_or_array, options = {}) if Hash === record_or_hash_or_array options = record_or_hash_or_array.merge(options) @@ -123,6 +128,10 @@ module ActionDispatch return polymorphic_path record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array, options], true) + end + opts = options.dup action = opts.delete :action type = :path @@ -134,7 +143,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 +157,195 @@ 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_path_for_action(action, record_or_hash, options) - polymorphic_path(record_or_hash, options.merge(:action => action)) - end - - class HelperMethodBuilder # :nodoc: - CACHE = { 'path' => {}, 'url' => {} } - - def self.get(action, type) - type = type.to_s - CACHE[type].fetch(action) { build action, type } + def polymorphic_url_for_action(action, record_or_hash, options) + polymorphic_url(record_or_hash, options.merge(action: action)) end - def self.url; CACHE['url'.freeze][nil]; end - def self.path; CACHE['path'.freeze][nil]; end + def polymorphic_path_for_action(action, record_or_hash, options) + polymorphic_path(record_or_hash, options.merge(action: action)) + end - def self.build(action, type) - prefix = action ? "#{action}_" : "" - suffix = type - if action.to_s == 'new' - HelperMethodBuilder.singular prefix, suffix + def polymorphic_mapping(record) + if record.respond_to?(:to_model) + _routes.polymorphic_mappings[record.to_model.model_name.name] else - HelperMethodBuilder.plural prefix, suffix + _routes.polymorphic_mappings[record.class.name] end end - def self.singular(prefix, suffix) - new(->(name) { name.singular_route_key }, prefix, suffix) - end + class HelperMethodBuilder # :nodoc: + CACHE = { "path" => {}, "url" => {} } - def self.plural(prefix, suffix) - new(->(name) { name.route_key }, prefix, suffix) - end + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } + end - def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) - builder = get action, type + def self.url; CACHE["url"][nil]; end + def self.path; CACHE["path"][nil]; end - 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 + 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 - - 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 end + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) + end - if options.empty? - recipient.send(method, *args) - else - recipient.send(method, *args, options) + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) end - end - attr_reader :suffix, :prefix + 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 - def initialize(key_strategy, prefix, suffix) - @key_strategy = key_strategy - @prefix = prefix - @suffix = suffix - end + when nil + raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array + end - def handle_string(record) - [get_method_for_string(record), []] - end + if options.empty? + recipient.send(method, *args) + else + recipient.send(method, *args, options) + end + end - def handle_string_call(target, str) - target.send get_method_for_string str - end + attr_reader :suffix, :prefix - def handle_class(klass) - [get_method_for_class(klass), []] - end + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix + end - def handle_class_call(target, klass) - target.send get_method_for_class klass - end + def handle_string(record) + [get_method_for_string(record), []] + end - def handle_model(record) - args = [] + def handle_string_call(target, str) + target.send get_method_for_string str + 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(klass) + [get_method_for_class(klass), []] + end - [named_route, args] - end + def handle_class_call(target, klass) + target.send get_method_for_class klass + end - def handle_model_call(target, model) - method, args = handle_model model - target.send(method, *args) - end + def handle_model(record) + args = [] - def handle_list(list) - record_list = list.dup - record = record_list.pop + 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 - args = [] + [named_route, args] + end - route = record_list.map { |parent| - case parent - when Symbol, String - parent.to_s - when Class - args << parent - parent.model_name.singular_route_key + def handle_model_call(target, record) + if mapping = polymorphic_mapping(target, record) + mapping.call(target, [record], suffix == "path") else - args << parent.to_model - parent.to_model.model_name.singular_route_key + method, args = handle_model(record) + target.send(method, *args) 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 + end + + 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 + record.to_s + when Class + @key_strategy.call record.model_name else - @key_strategy.call model.model_name + model = record.to_model + if model.persisted? + args << model + model.model_name.singular_route_key + else + @key_strategy.call model.model_name + end 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 polymorphic_mapping(target, record) + if record.respond_to?(:to_model) + target._routes.polymorphic_mappings[record.to_model.model_name.name] + else + target._routes.polymorphic_mappings[record.class.name] + end + end - def get_method_for_string(str) - "#{prefix}#{str}_#{suffix}" - end + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + get_method_for_string name + end - [nil, 'new', 'edit'].each do |action| - CACHE['url'][action] = build action, 'url' - CACHE['path'][action] = build action, 'path' + 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" + 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 d6987f4d09..143a4b3d62 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -1,9 +1,11 @@ -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' +# frozen_string_literal: true + +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,7 +24,6 @@ module ActionDispatch end def serve(req) - req.check_path_parameters! uri = URI.parse(path(req.path_parameters, req)) unless uri.host @@ -37,12 +38,14 @@ module ActionDispatch uri.host ||= req.host uri.port ||= req.port unless req.standard_port? + req.commit_flash + 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 +61,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 +107,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*\}/) @@ -129,21 +132,23 @@ module ActionDispatch 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") # + # This will redirect the user, while ignoring certain parts of the request, including query string, etc. + # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, etc all redirect to <tt>/posts</tt>. + # # You can also use interpolation in the supplied redirect argument: # # get 'docs/:article', to: redirect('/wiki/%{article}') # - # Note that if you return a path without a leading slash then the url is prefixed with the + # Note that if you return a path without a leading slash then the URL is prefixed with the # current SCRIPT_NAME environment variable. This is typically '/' but may be different in # a mounted engine or where the application is deployed to a subdirectory of a website. # @@ -162,11 +167,16 @@ module ActionDispatch # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead. # - # The options version of redirect allows you to supply only the parts of the url which need + # 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. # # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}') # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}') + # get '/stories', to: redirect(path: '/posts') + # + # This will redirect the user, while changing only the specified parts of the request, + # for example the +path+ option in the last example. + # <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, redirect to <tt>/posts</tt> and <tt>/posts?foo=bar</tt> respectively. # # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse # common redirect routes. The call method must accept two arguments, params and request, and return diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 85f202b823..d0a7eadf45 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,12 +1,13 @@ -require 'action_dispatch/journey' -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' +# frozen_string_literal: true + +require "action_dispatch/journey" +require "active_support/core_ext/object/to_query" +require "active_support/core_ext/module/redefine_method" +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 @@ -34,7 +35,7 @@ module ActionDispatch if @raise_on_name_error raise else - return [404, {'X-Cascade' => 'pass'}, []] + [404, { "X-Cascade" => "pass" }, []] end end @@ -59,7 +60,7 @@ module ActionDispatch private - def controller(_); @controller_class; end + def controller(_); @controller_class; end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -71,7 +72,7 @@ module ActionDispatch private :routes def initialize - @routes = {} + @routes = {} @path_helpers = Set.new @url_helpers = Set.new @url_helpers_module = Module.new @@ -89,11 +90,11 @@ module ActionDispatch def clear! @path_helpers.each do |helper| - @path_helpers_module.send :undef_method, helper + @path_helpers_module.remove_method helper end @url_helpers.each do |helper| - @url_helpers_module.send :undef_method, helper + @url_helpers_module.remove_method helper end @routes.clear @@ -107,8 +108,8 @@ module ActionDispatch url_name = :"#{name}_url" if routes.key? key - @path_helpers_module.send :undef_method, path_name - @url_helpers_module.send :undef_method, url_name + @path_helpers_module.undef_method path_name + @url_helpers_module.undef_method url_name end routes[key] = route define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH @@ -144,6 +145,31 @@ module ActionDispatch routes.length end + # Given a +name+, defines name_path and name_url helpers. + # Used by 'direct', 'resolve', and 'polymorphic' route helpers. + def add_url_helper(name, defaults, &block) + helper = CustomUrlHelper.new(name, defaults, &block) + path_name = :"#{name}_path" + url_name = :"#{name}_url" + + @path_helpers_module.module_eval do + redefine_method(path_name) do |*args| + helper.call(self, args, true) + end + end + + @url_helpers_module.module_eval do + redefine_method(url_name) do |*args| + helper.call(self, args, false) + end + end + + @path_helpers << path_name + @url_helpers << url_name + + self + end + class UrlHelper def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) @@ -172,6 +198,16 @@ module ActionDispatch if args.size == arg_size && !inner_options && optimize_routes_generation?(t) options = t.url_options.merge @options options[:path] = optimized_helper(args) + + original_script_name = options.delete(:original_script_name) + script_name = t._routes.find_script_name(options) + + if original_script_name + script_name = original_script_name + script_name + end + + options[:script_name] = script_name + url_strategy.call options else super @@ -180,40 +216,40 @@ module ActionDispatch private - def optimized_helper(args) - params = parameterize_args(args) do - raise_generation_error(args) - end + 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) @@ -248,6 +284,8 @@ module ActionDispatch if args.size < path_params_size path_params -= controller_options.keys path_params -= result.keys + else + path_params = path_params.dup end inner_options.each_key do |key| path_params.delete(key) @@ -264,53 +302,50 @@ 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| - last = args.last - options = case last - when Hash - args.pop - when ActionController::Parameters - if last.permitted? - args.pop.to_h - else - raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!" - end - end - 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 + args.pop.to_h + end + helper.call self, args, options + end end end - end end - # strategy for building urls to send to the client + # strategy for building URLs to send to the client PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options - attr_reader :env_key + attr_reader :env_key, :polymorphic_mappings alias :routes :set def self.default_resources_path_names - { :new => 'new', :edit => 'edit' } + { new: "new", edit: "edit" } end def self.new_with_config(config) @@ -342,11 +377,18 @@ module ActionDispatch @prepend = [] @disable_clear_and_finalize = false @finalized = false - @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze + @env_key = "ROUTES_#{object_id}_SCRIPT_NAME" @set = Journey::Routes.new @router = Journey::Router.new @set @formatter = Journey::Formatter.new self + @polymorphic_mappings = {} + end + + def eager_load! + router.eager_load! + routes.each(&:eager_load!) + nil end def relative_url_root @@ -402,6 +444,7 @@ module ActionDispatch named_routes.clear set.clear formatter.clear + @polymorphic_mappings.clear @prepend.each { |blk| eval_block(blk) } end @@ -418,7 +461,7 @@ module ActionDispatch MountedHelpers end - def define_mounted_helper(name) + def define_mounted_helper(name, script_namer = nil) return if MountedHelpers.method_defined?(name) routes = self @@ -426,7 +469,7 @@ module ActionDispatch MountedHelpers.class_eval do define_method "_#{name}" do - RoutesProxy.new(routes, _routes_context, helpers) + RoutesProxy.new(routes, _routes_context, helpers, script_namer) end end @@ -446,17 +489,50 @@ module ActionDispatch # Define url_for in the singleton level so one can do: # Rails.application.routes.url_helpers.url_for(args) - @_routes = routes + proxy_class = Class.new do + include UrlFor + include routes.named_routes.path_helpers_module + include routes.named_routes.url_helpers_module + + attr_reader :_routes + + def initialize(routes) + @_routes = routes + end + + def optimize_routes_generation? + @_routes.optimize_routes_generation? + end + end + + @_proxy = proxy_class.new(routes) + class << self def url_for(options) - @_routes.url_for(options) + @_proxy.url_for(options) + end + + def full_url_for(options) + @_proxy.full_url_for(options) + end + + def route_for(name, *args) + @_proxy.route_for(name, *args) end def optimize_routes_generation? - @_routes.optimize_routes_generation? + @_proxy.optimize_routes_generation? end - attr_reader :_routes + def polymorphic_url(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_url(record_or_hash_or_array, options) + end + + def polymorphic_path(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_path(record_or_hash_or_array, options) + end + + def _routes; @_proxy._routes; end def url_options; {}; end end @@ -480,7 +556,7 @@ module ActionDispatch # plus a singleton class method called _routes ... included do - singleton_class.send(:redefine_method, :_routes) { routes } + redefine_singleton_method(:_routes) { routes } end # And an instance method _routes. Note that @@ -500,7 +576,7 @@ module ActionDispatch routes.empty? end - def add_route(mapping, path_ast, name, anchor) + def add_route(mapping, name) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) if name && named_routes[name] @@ -508,7 +584,7 @@ module ActionDispatch "You may have defined two routes with the same name using the `:as` option, or " \ "you may be overriding a route already defined by a resource with the same naming. " \ "For the latter, you can restrict the routes created with `resources` as explained here: \n" \ - "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" + "https://guides.rubyonrails.org/routing.html#restricting-the-routes-created" end route = @set.add_route(name, mapping) @@ -517,20 +593,58 @@ module ActionDispatch 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 + will be removed in Rails 6.0. MSG end 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 + will be removed in Rails 6.0. MSG end route end + def add_polymorphic_mapping(klass, options, &block) + @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block) + end + + def add_url_helper(name, options, &block) + named_routes.add_url_helper(name, options, &block) + end + + class CustomUrlHelper + attr_reader :name, :defaults, :block + + def initialize(name, defaults, &block) + @name = name + @defaults = defaults + @block = block + end + + def call(t, args, only_path = false) + options = args.extract_options! + url = t.full_url_for(eval_block(t, args, options)) + + if only_path + "/" + url.partition(%r{(?<!/)/(?!/)}).last + else + url + end + end + + private + def eval_block(t, args, options) + t.instance_exec(*args, merge_defaults(options), &block) + end + + def merge_defaults(options) + defaults ? defaults.merge(options) : options + end + end + class Generator PARAMETERIZE = lambda do |name, value| if name == :controller @@ -548,12 +662,10 @@ module ActionDispatch @recall = recall @set = set - normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - normalize_action! end def controller @@ -572,11 +684,6 @@ module ActionDispatch 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 @@ -588,12 +695,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 @@ -603,8 +710,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 @@ -612,7 +719,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("/") @@ -622,7 +729,7 @@ module ActionDispatch # Remove leading slashes from controllers def normalize_controller! if controller - if controller.start_with?("/".freeze) + if controller.start_with?("/") @options[:controller] = controller[1..-1] else @options[:controller] = controller @@ -630,13 +737,6 @@ module ActionDispatch end end - # Move 'index' action from options to recall - def normalize_action! - if @options[:action] == 'index'.freeze - @recall[:action] = @options.delete(:action) - end - end - # Generates a path from routes, returns [path, params]. # If no route is generated the formatter will raise ActionController::UrlGenerationError def generate @@ -660,11 +760,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 @@ -684,7 +784,7 @@ module ActionDispatch end def find_script_name(options) - options.delete(:script_name) || find_relative_url_root(options) || '' + options.delete(:script_name) || find_relative_url_root(options) || "" end def find_relative_url_root(options) @@ -706,7 +806,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 @@ -745,12 +845,16 @@ 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 = make_request(env) + recognize_path_with_request(req, path, extras) + end + + def recognize_path_with_request(req, path, extras, raise_on_missing: true) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -759,8 +863,7 @@ module ActionDispatch params[key] = URI.parser.unescape(value) end end - old_params = req.path_parameters - req.path_parameters = old_params.merge params + req.path_parameters = params app = route.app if app.matches?(req) && app.dispatcher? begin @@ -770,10 +873,15 @@ module ActionDispatch end return req.path_parameters + elsif app.matches?(req) && app.engine? + path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false) + return path_parameters if path_parameters end end - raise ActionController::RoutingError, "No route matches #{path.inspect}" + if raise_on_missing + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end end end # :startdoc: diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index 040ea04046..587a72729c 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/array/extract_options' +# frozen_string_literal: true + +require "active_support/core_ext/array/extract_options" module ActionDispatch module Routing @@ -8,9 +10,10 @@ module ActionDispatch attr_accessor :scope, :routes alias :_routes :routes - def initialize(routes, scope, helpers) + def initialize(routes, scope, helpers, script_namer = nil) @routes, @scope = routes, scope @helpers = helpers + @script_namer = script_namer end def url_options @@ -19,7 +22,8 @@ module ActionDispatch end end - def respond_to?(method, include_private = false) + private + def respond_to_missing?(method, _) super || @helpers.respond_to?(method) end @@ -28,15 +32,38 @@ module ActionDispatch self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args) options = args.extract_options! - args << url_options.merge((options || {}).symbolize_keys) + options = url_options.merge((options || {}).symbolize_keys) + + if @script_namer + options[:script_name] = merge_script_names( + options[:script_name], + @script_namer.call(options) + ) + end + + args << options @helpers.#{method}(*args) end RUBY - send(method, *args) + public_send(method, *args) else super end end + + # Keeps the part of the script name provided by the global + # context via ENV["SCRIPT_NAME"], which `mount` doesn't know + # about since it depends on the specific request, but use our + # script name resolver for the mount point dependent part. + def merge_script_names(previous_script_name, new_script_name) + return new_script_name unless previous_script_name + + resolved_parts = new_script_name.count("/") + previous_parts = previous_script_name.count("/") + context_parts = previous_parts - resolved_parts + 1 + + (previous_script_name.split("/").slice(0, context_parts).join("/")) + new_script_name + end end end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 28be189f93..fcb8ae296b 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionDispatch module Routing # In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse @@ -107,16 +109,16 @@ module ActionDispatch end # Hook overridden in controller to add request information - # with `default_url_options`. Application logic should not + # with +default_url_options+. Application logic should not # go into url_options. def url_options default_url_options end - # Generate a url based on the options provided, default_url_options and the + # Generate a URL based on the options provided, default_url_options and the # routes defined in routes.rb. The following options are supported: # - # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. + # * <tt>:only_path</tt> - If true, the relative URL is returned. Defaults to +false+. # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. # * <tt>:host</tt> - Specifies the host the link should be targeted at. # If <tt>:only_path</tt> is false, this option must be @@ -131,6 +133,7 @@ module ActionDispatch # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. + # * <tt>:params</tt> - The query parameters to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path. # @@ -153,7 +156,7 @@ module ActionDispatch # 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`: + # through <tt>GET /users/1</tt>: # # url_for(only_path: true) # => '/users/1' # url_for(only_path: true, action: 'edit') # => '/users/1/edit' @@ -164,20 +167,17 @@ module ActionDispatch # implicitly used by +url_for+ can always be overwritten like shown on the # last +url_for+ calls. def url_for(options = nil) + full_url_for(options) + end + + def full_url_for(options = nil) # :nodoc: case options when nil _routes.url_for(url_options.symbolize_keys) - when Hash - 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("Generating a URL from non sanitized request parameters is insecure!") - end + when Hash, ActionController::Parameters route_name = options.delete :use_route - _routes.url_for(options.to_h.symbolize_keys. - reverse_merge!(url_options), route_name) + merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options) + _routes.url_for(merged_url_options, route_name) when String options when Symbol @@ -192,22 +192,46 @@ module ActionDispatch end end + # Allows calling direct or regular named route. + # + # resources :buckets + # + # direct :recordable do |recording| + # route_for(:bucket, recording.bucket) + # end + # + # direct :threadable do |threadable| + # route_for(:recordable, threadable.parent) + # end + # + # This maintains the context of the original caller on + # whether to return a path or full URL, e.g: + # + # threadable_path(threadable) # => "/buckets/1" + # threadable_url(threadable) # => "http://example.com/buckets/1" + # + def route_for(name, *args) + public_send(:"#{name}_url", *args) + end + 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 + private - def _routes_context - self - end + def _with_routes(routes) # :doc: + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end + + def _routes_context # :doc: + self + end end end end |