aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGannon McGibbon <gannon.mcgibbon@gmail.com>2019-05-22 20:15:18 -0400
committerGitHub <noreply@github.com>2019-05-22 20:15:18 -0400
commiteba859d867454cd441f29ff15d8dbf442e51919c (patch)
tree4e59668361d15dd110bce665105d1f60325f2ae7
parentd5a2f7ec148726d7547e367d7a968e3b4be9b509 (diff)
parent20104ba13daae81184a339b8949054aecbbd7655 (diff)
downloadrails-eba859d867454cd441f29ff15d8dbf442e51919c.tar.gz
rails-eba859d867454cd441f29ff15d8dbf442e51919c.tar.bz2
rails-eba859d867454cd441f29ff15d8dbf442e51919c.zip
Merge pull request #34656 from albertoalmagro/path_helper_without_params_does_not_loose_scope
Keep part when scope option has value
-rw-r--r--actionpack/CHANGELOG.md11
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb12
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb40
-rw-r--r--actionpack/test/dispatch/routing_test.rb41
-rw-r--r--actionpack/test/journey/route_test.rb41
6 files changed, 100 insertions, 47 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index a84f72eca9..4bcf313ecc 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,14 @@
+* Keep part when scope option has value
+
+ When a route was defined within an optional scope, if that route didn't
+ take parameters the scope was lost when using path helpers. This commit
+ ensures scope is kept both when the route takes parameters or when it
+ doesn't.
+
+ Fixes #33219
+
+ *Alberto Almagro*
+
* Added `deep_transform_keys` and `deep_transform_keys!` methods to ActionController::Parameters.
*Gustavo Gutierrez*
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 52396ec901..53c404ee7c 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -67,7 +67,7 @@ module ActionDispatch
parameterized_parts = recall.merge(options)
keys_to_keep = route.parts.reverse_each.drop_while { |part|
- !options.key?(part) || (options[part] || recall[part]).nil?
+ !(options.key?(part) || route.scope_options.key?(part)) || (options[part] || recall[part]).nil?
} | route.required_parts
parameterized_parts.delete_if do |bad_key, _|
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 8165709a3d..4aee7a6f83 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -4,9 +4,9 @@ module ActionDispatch
# :stopdoc:
module Journey
class Route
- attr_reader :app, :path, :defaults, :name, :precedence
+ attr_reader :app, :path, :defaults, :name, :precedence, :constraints,
+ :internal, :scope_options
- attr_reader :constraints, :internal
alias :conditions :constraints
module VerbMatchers
@@ -49,15 +49,10 @@ module ActionDispatch
end
end
- def self.build(name, app, path, constraints, required_defaults, defaults)
- request_method_match = verb_matcher(constraints.delete(:request_method))
- new name, app, path, constraints, required_defaults, defaults, request_method_match, 0
- end
-
##
# +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route.
- def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false)
+ def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false)
@name = name
@app = app
@path = path
@@ -72,6 +67,7 @@ module ActionDispatch
@decorated_ast = nil
@precedence = precedence
@path_formatter = @path.build_formatter
+ @scope_options = scope_options
@internal = internal
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index f29f66990d..0aba484a45 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -70,17 +70,21 @@ module ActionDispatch
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
- attr_reader :required_defaults, :ast
+ attr_reader :requirements, :defaults, :to, :default_controller,
+ :default_action, :required_defaults, :ast, :scope_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]
-
- defaults = (scope[:defaults] || {}).dup
- scope_constraints = scope[:constraints] || {}
+ scope_params = {
+ blocks: scope[:blocks] || [],
+ constraints: scope[:constraints] || {},
+ defaults: (scope[:defaults] || {}).dup,
+ module: scope[:module],
+ options: scope[:options] || {}
+ }
- new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
+ new set: set, ast: ast, controller: controller, default_action: default_action,
+ to: to, formatted: formatted, via: via, options_constraints: options_constraints,
+ anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
end
def self.check_via(via)
@@ -111,10 +115,9 @@ module ActionDispatch
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)
- @defaults = defaults
- @set = set
-
+ def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
+ @defaults = scope_params[:defaults]
+ @set = set
@to = intern(to)
@default_controller = intern(controller)
@default_action = intern(default_action)
@@ -122,22 +125,23 @@ module ActionDispatch
@anchor = anchor
@via = via
@internal = options.delete(:internal)
+ @scope_options = scope_params[:options]
path_params = ast.find_all(&:symbol?).map(&:to_sym)
options = add_wildcard_options(options, formatted, ast)
- options = normalize_options!(options, path_params, modyoule)
+ options = normalize_options!(options, path_params, scope_params[:module])
split_options = constraints(options, path_params)
- constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
+ constraints = scope_params[:constraints].merge Hash[split_options[: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
+ @blocks = scope_params[:blocks]
constraints.merge! options_constraints
else
@blocks = blocks(options_constraints)
@@ -160,8 +164,10 @@ module ActionDispatch
end
def make_route(name, precedence)
- Journey::Route.new(name, application, path, conditions, required_defaults,
- defaults, request_method, precedence, @internal)
+ Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
+ required_defaults: required_defaults, defaults: defaults,
+ request_method_match: request_method, precedence: precedence,
+ scope_options: scope_options, internal: @internal)
end
def application
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 362488d585..0070d7af72 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -4959,6 +4959,47 @@ class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest
end
end
+class TestOptionalScopesWithOrWithoutParams < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ scope module: "test_optional_scopes_with_or_without_params" do
+ scope "(:locale)", locale: /en|es/ do
+ get "home", to: "home#index"
+ get "with_param/:foo", to: "home#with_param", as: "with_param"
+ get "without_param", to: "home#without_param"
+ end
+ end
+ end
+ end
+
+ class HomeController < ActionController::Base
+ include Routes.url_helpers
+
+ def index
+ render inline: "<%= with_param_path(foo: 'bar') %> | <%= without_param_path %>"
+ end
+
+ def with_param; end
+ def without_param; end
+ end
+
+ APP = build_app Routes
+
+ def app
+ APP
+ end
+
+ def test_stays_unscoped_with_or_without_params
+ get "/home"
+ assert_equal "/with_param/bar | /without_param", response.body
+ end
+
+ def test_preserves_scope_with_or_without_params
+ get "/es/home"
+ assert_equal "/es/with_param/bar | /es/without_param", response.body
+ end
+end
+
class TestPathParameters < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb
index a8bf4a11e2..8828201e4f 100644
--- a/actionpack/test/journey/route_test.rb
+++ b/actionpack/test/journey/route_test.rb
@@ -9,7 +9,7 @@ module ActionDispatch
app = Object.new
path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
defaults = {}
- route = Route.build("name", app, path, {}, [], defaults)
+ route = Route.new(name: "name", app: app, path: path, defaults: defaults)
assert_equal app, route.app
assert_equal path, route.path
@@ -17,10 +17,9 @@ module ActionDispatch
end
def test_route_adds_itself_as_memo
- app = Object.new
- path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
- defaults = {}
- route = Route.build("name", app, path, {}, [], defaults)
+ app = Object.new
+ path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
+ route = Route.new(name: "name", app: app, path: path)
route.ast.grep(Nodes::Terminal).each do |node|
assert_equal route, node.memo
@@ -28,30 +27,30 @@ module ActionDispatch
end
def test_path_requirements_override_defaults
- path = Path::Pattern.build(":name", { name: /love/ }, "/", true)
- defaults = { name: "tender" }
- route = Route.build("name", nil, path, {}, [], defaults)
+ path = Path::Pattern.build(":name", { name: /love/ }, "/", true)
+ defaults = { name: "tender" }
+ route = Route.new(name: "name", path: path, defaults: defaults)
assert_equal(/love/, route.requirements[:name])
end
def test_ip_address
path = Path::Pattern.from_string "/messages/:id(.:format)"
- route = Route.build("name", nil, path, { ip: "192.168.1.1" }, [],
- controller: "foo", action: "bar")
+ route = Route.new(name: "name", path: path, constraints: { ip: "192.168.1.1" },
+ defaults: { controller: "foo", action: "bar" })
assert_equal "192.168.1.1", route.ip
end
def test_default_ip
path = Path::Pattern.from_string "/messages/:id(.:format)"
- route = Route.build("name", nil, path, {}, [],
- controller: "foo", action: "bar")
+ route = Route.new(name: "name", path: path,
+ defaults: { controller: "foo", action: "bar" })
assert_equal(//, route.ip)
end
def test_format_with_star
path = Path::Pattern.from_string "/:controller/*extra"
- route = Route.build("name", nil, path, {}, [],
- controller: "foo", action: "bar")
+ route = Route.new(name: "name", path: path,
+ defaults: { controller: "foo", action: "bar" })
assert_equal "/foo/himom", route.format(
controller: "foo",
extra: "himom")
@@ -59,7 +58,8 @@ module ActionDispatch
def test_connects_all_match
path = Path::Pattern.from_string "/:controller(/:action(/:id(.:format)))"
- route = Route.build("name", nil, path, { action: "bar" }, [], controller: "foo")
+ route = Route.new(name: "name", path: path, constraints: { action: "bar" },
+ defaults: { controller: "foo" })
assert_equal "/foo/bar/10", route.format(
controller: "foo",
@@ -69,34 +69,33 @@ module ActionDispatch
def test_extras_are_not_included_if_optional
path = Path::Pattern.from_string "/page/:id(/:action)"
- route = Route.build("name", nil, path, {}, [], action: "show")
+ route = Route.new(name: "name", path: path, defaults: { action: "show" })
assert_equal "/page/10", route.format(id: 10)
end
def test_extras_are_not_included_if_optional_with_parameter
path = Path::Pattern.from_string "(/sections/:section)/pages/:id"
- route = Route.build("name", nil, path, {}, [], action: "show")
+ route = Route.new(name: "name", path: path, defaults: { action: "show" })
assert_equal "/pages/10", route.format(id: 10)
end
def test_extras_are_not_included_if_optional_parameter_is_nil
path = Path::Pattern.from_string "(/sections/:section)/pages/:id"
- route = Route.build("name", nil, path, {}, [], action: "show")
+ route = Route.new(name: "name", path: path, defaults: { action: "show" })
assert_equal "/pages/10", route.format(id: 10, section: nil)
end
def test_score
- constraints = {}
defaults = { controller: "pages", action: "show" }
path = Path::Pattern.from_string "/page/:id(/:action)(.:format)"
- specific = Route.build "name", nil, path, constraints, [:controller, :action], defaults
+ specific = Route.new name: "name", path: path, required_defaults: [:controller, :action], defaults: defaults
path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)"
- generic = Route.build "name", nil, path, constraints, [], {}
+ generic = Route.new name: "name", path: path
knowledge = { "id" => true, "controller" => true, "action" => true }