Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore to: option in routes with an implicit controller #51523

Merged
merged 1 commit into from
Apr 17, 2024

Conversation

etiennebarrie
Copy link
Contributor

Ref #50465

In #50466, routes that use the to: option without providing a controller (e.g. to: "action") started to raise an ArgumentError. Before that it was possible to use the to: option without a controller as long as it was nested within a controller, resource or resources scope.

This broke one of our apps between 7.1.2 and 7.1.3, here's a repro script:

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "rails", ARGV.first || "7.1.3"
end

require "action_controller/railtie"
require "minitest/autorun"

class FoosController < ActionController::Base
end

class BugTest < ActionController::TestCase
  def test_routing
    with_routing do |routes|
      routes.draw do
        resources :foos, only: [] do
          get "bar", to: "bar"
        end
      end

      assert_routing "/foos/1/bar", controller: "foos", action: "bar", foo_id: "1"
    end
  end
end

This fails using 7.1.3 and passes using 7.1.2:

$ ruby route-implicit-controller.rb |tail -1
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
$ ruby route-implicit-controller.rb 7.1.2 |tail -1                                                                                                                                                                                 
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips

This PR fixes this and also allows passing a Symbol in those cases (e.g. to: :action).

The :to option for routes can once again be a Symbol or a String without a controller if the controller is implicitly provided by a nesting controller or resources call.

With these changes, the documentation changes from #50523 would not have been necessary, though it's fine to keep the documentation as it is now.

cc @skipkayhil @p8

ex = assert_raises(ArgumentError) {
draw do
get "/foo/bar", to: "foo#"
end
}
assert_match(/Missing :action/, ex.message)
assert_match(/:to must respond to/, ex.message)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to fail in check_controller_and_action, whereas now it fails in the checks for the to: option, which seems correct to me.

@@ -4037,6 +4037,7 @@ def draw(&block)
routes = ActionDispatch::Routing::RouteSet.new
routes.draw(&block)
@app = self.class.build_app routes
@routes = routes
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lets us use routing assertions directly without having to use with_routing.

raise ArgumentError, ":to must respond to `action` or `call`, or it must be a String that includes '#'"
if to.is_a?(String)
controller_or_action, action = to.split("#", 2).map!(&:-@)
if !action || action.empty?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I avoided using action.present? here, but maybe I could just require the core ext.

else
raise ArgumentError, ":to must respond to `action` or `call`, or it must be a String that includes '#'"
if to.is_a?(String)
controller_or_action, action = to.split("#", 2).map!(&:-@)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifying the numbers of splits here makes sure we handle the "controller#" case correctly (and that we don't assume controller is the action).

Copy link
Member

@skipkayhil skipkayhil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a CHANGELOG entry since this fixes a regression in 7.1.3?

elsif to.is_a?(Symbol)
action = to
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the goal is to backport this to 7-1-stable I'm not sure we should be adding support for Symbol since that didn't work even before #50466

else
raise ArgumentError, ":to must respond to `action` or `call`, or it must be a String that includes '#'"
if to.is_a?(String)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this new code is much harder to follow than before #50466.

What do you think of this diff? It also passes the reproduction script and I think is a bit simpler:

diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 724bf72e5c..b152de0ea7 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -231,10 +231,15 @@ def normalize_options!(options, path_params, modyoule)
               if to.nil?
                 controller = default_controller
                 action = default_action
-              elsif to.is_a?(String) && to.include?("#")
-                to_endpoint = to.split("#").map!(&:-@)
-                controller  = to_endpoint[0]
-                action      = to_endpoint[1]
+              elsif to.is_a?(String)
+                if to.include?("#")
+                  to_endpoint = to.split("#").map!(&:-@)
+                  controller  = to_endpoint[0]
+                  action      = to_endpoint[1]
+                else
+                  controller = default_controller
+                  action = default_action
+                end
               else
                 raise ArgumentError, ":to must respond to `action` or `call`, or it must be a String that includes '#'"
               end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's nicer. I had to use to instead of default_action in the new branch handling the string when it's missing the #, otherwise the repro script wasn't passing.

@etiennebarrie
Copy link
Contributor Author

etiennebarrie commented Apr 10, 2024

Added a changelog entry but it's a bit weird since the previous change in 7.1.3 hadn't been mentioned in the first place.

The `:to` option for routes can once again be a String without a
controller if the controller is implicitly provided by a nesting
`controller` or `resources` call.
@skipkayhil skipkayhil added this to the 7.1.4 milestone Apr 10, 2024
@byroot byroot merged commit b272be8 into rails:main Apr 17, 2024
4 checks passed
byroot added a commit that referenced this pull request Apr 17, 2024
…ller

Restore `to:` option in routes with an implicit controller
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants