diff --git a/lib/rage/cable/cable.rb b/lib/rage/cable/cable.rb index a6de3d3f..e552343f 100644 --- a/lib/rage/cable/cable.rb +++ b/lib/rage/cable/cable.rb @@ -53,7 +53,11 @@ def self.application end end - Rage.with_middlewares(application, Rage.config.cable.middlewares) + chain = Rage.with_middlewares(application, Rage.config.cable.middlewares) + application.define_singleton_method(:__rage_app_name) { "Rage::Cable" } + chain.define_singleton_method(:__rage_root_app) { application } + + chain end # @private diff --git a/lib/rage/cli.rb b/lib/rage/cli.rb index a99191b4..f2881e63 100644 --- a/lib/rage/cli.rb +++ b/lib/rage/cli.rb @@ -154,7 +154,12 @@ def routes meta = route[:constraints] meta.merge!(route[:defaults]) if route[:defaults] - handler = route[:meta][:raw_handler] + raw_handler = route[:meta][:raw_handler] + handler = if raw_handler.respond_to?(:__rage_app_name) + raw_handler.__rage_app_name + else + raw_handler + end handler = "#{handler} #{meta}" unless meta&.empty? puts format("%-#{longest_method}s%-#{longest_path}s%s", route[:method], route[:path], handler) diff --git a/lib/rage/openapi/openapi.rb b/lib/rage/openapi/openapi.rb index 7194ddf3..861ccc3f 100644 --- a/lib/rage/openapi/openapi.rb +++ b/lib/rage/openapi/openapi.rb @@ -67,13 +67,17 @@ def self.application(namespace: nil) end end - if Rage.config.middleware.include?(Rage::Reloader) + chain = if Rage.config.middleware.include?(Rage::Reloader) Rage.with_middlewares(app, [Rage::Reloader]) elsif defined?(ActionDispatch::Reloader) && Rage.config.middleware.include?(ActionDispatch::Reloader) Rage.with_middlewares(app, [ActionDispatch::Reloader]) else app end + + chain.define_singleton_method(:__rage_app_name) { "Rage::OpenAPI" } + + chain end # Build an OpenAPI specification for the application. diff --git a/lib/rage/router/backend.rb b/lib/rage/router/backend.rb index f388f478..9e59f0d6 100644 --- a/lib/rage/router/backend.rb +++ b/lib/rage/router/backend.rb @@ -22,8 +22,13 @@ def reset_routes def mount(path, handler, methods) raise ArgumentError, "Mount handler should respond to `call`" unless handler.respond_to?(:call) - raw_handler = handler - handler = wrap_in_rack_session(handler) if handler.respond_to?(:name) && handler.name == "Sidekiq::Web" + raw_handler = handler.respond_to?(:__rage_root_app) ? handler.__rage_root_app : handler + + handler = if handler.respond_to?(:name) && handler.name == "Sidekiq::Web" + wrap_in_rack_session(handler) + else + raw_handler + end app = ->(env, _params) do # rewind `rack.input` in case mounted application needs to access the request body; diff --git a/spec/rage/cli_spec.rb b/spec/rage/cli_spec.rb index a66f9c43..992f0c14 100644 --- a/spec/rage/cli_spec.rb +++ b/spec/rage/cli_spec.rb @@ -601,4 +601,176 @@ def def_subscriber(name, subscribe_to:) end end end + + describe "#routes" do + subject { rage_cli.routes } + + let(:router) { instance_double("Rage::Router::Backend") } + + before do + allow(rage_cli).to receive(:environment) + allow(Rage).to receive(:__router).and_return(router) + allow(router).to receive(:routes).and_return(routes) + end + + context "when there are no routes" do + let(:routes) { [] } + + it "outputs only the header" do + expect { subject }.to output(/Verb\s+Path\s+Controller#Action/).to_stdout + end + end + + context "when there is a single route" do + let(:routes) do + [ + { method: "GET", path: "/", meta: { raw_handler: "application#index" }, constraints: {}, defaults: nil } + ] + end + + it "outputs the route" do + expect { subject }.to output(/GET\s+\/\s+application#index/).to_stdout + end + end + + context "when there are multiple routes" do + let(:routes) do + [ + { method: "GET", path: "/users", meta: { raw_handler: "users#index" }, constraints: {}, defaults: nil }, + { method: "POST", path: "/users", meta: { raw_handler: "users#create" }, constraints: {}, defaults: nil }, + { method: "GET", path: "/users/:id", meta: { raw_handler: "users#show" }, constraints: {}, defaults: nil } + ] + end + + it "outputs all routes" do + output = capture_stdout { subject } + + expect(output).to match(/GET\s+\/users\s+users#index/) + expect(output).to match(/POST\s+\/users\s+users#create/) + expect(output).to match(/GET\s+\/users\/:id\s+users#show/) + end + end + + context "when routes have the same path and handler" do + let(:routes) do + [ + { method: "GET", path: "/resource", meta: { raw_handler: "resource#action" }, constraints: {}, defaults: nil }, + { method: "POST", path: "/resource", meta: { raw_handler: "resource#action" }, constraints: {}, defaults: nil } + ] + end + + it "groups the methods with a pipe" do + expect { subject }.to output(/GET\|POST\s+\/resource\s+resource#action/).to_stdout + end + end + + context "when a route has constraints" do + let(:routes) do + [ + { method: "GET", path: "/users/:id", meta: { raw_handler: "users#show" }, constraints: { id: /\d+/ }, defaults: nil } + ] + end + + it "outputs the route with constraints" do + expect { subject }.to output(/GET\s+\/users\/:id\s+users#show \{:?id(:|=>)/).to_stdout + end + end + + context "when a route has defaults" do + let(:routes) do + [ + { method: "GET", path: "/users", meta: { raw_handler: "users#index" }, constraints: {}, defaults: { format: :json } } + ] + end + + it "outputs the route with defaults" do + expect { subject }.to output(/GET\s+\/users\s+users#index \{:?format(:|=>)\s?:json\}/).to_stdout + end + end + + context "when filtering routes with --grep option" do + subject { rage_cli.routes } + + let(:routes) do + [ + { method: "GET", path: "/users", meta: { raw_handler: "users#index" }, constraints: {}, defaults: nil }, + { method: "GET", path: "/posts", meta: { raw_handler: "posts#index" }, constraints: {}, defaults: nil }, + { method: "POST", path: "/users", meta: { raw_handler: "users#create" }, constraints: {}, defaults: nil } + ] + end + + let(:rage_cli) { described_class.new([], grep: "users") } + + it "only outputs matching routes" do + output = capture_stdout { subject } + + expect(output).to match(/users#index/) + expect(output).to match(/users#create/) + expect(output).not_to match(/posts#index/) + end + end + + context "when filtering routes by method" do + subject { rage_cli.routes } + + let(:routes) do + [ + { method: "GET", path: "/users", meta: { raw_handler: "users#index" }, constraints: {}, defaults: nil }, + { method: "POST", path: "/users", meta: { raw_handler: "users#create" }, constraints: {}, defaults: nil } + ] + end + + let(:rage_cli) { described_class.new([], grep: "POST") } + + it "only outputs routes matching the method" do + output = capture_stdout { subject } + + expect(output).to match(/POST\s+\/users\s+users#create/) + expect(output).not_to match(/GET\s+\/users\s+users#index/) + end + end + + context "when a route is a mounted app" do + let(:mounted_app) do + Class.new do + def self.__rage_app_name + "Sidekiq::Web" + end + end + end + + let(:routes) do + [ + { method: "GET", path: "/sidekiq", meta: { raw_handler: mounted_app, mount: true }, constraints: {}, defaults: nil } + ] + end + + it "outputs the mounted app name" do + expect { subject }.to output(/\s+\/sidekiq\s+Sidekiq::Web/).to_stdout + end + end + + context "when a mounted route ends with *" do + let(:routes) do + [ + { method: "GET", path: "/sidekiq/*", meta: { raw_handler: "SidekiqWeb", mount: true }, constraints: {}, defaults: nil } + ] + end + + it "does not output the route" do + output = capture_stdout { subject } + + expect(output).not_to match(/\/sidekiq\/\*/) + end + end + + def capture_stdout + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end + end end diff --git a/spec/router/mount_spec.rb b/spec/router/mount_spec.rb index ac466430..a324bd73 100644 --- a/spec/router/mount_spec.rb +++ b/spec/router/mount_spec.rb @@ -102,4 +102,34 @@ def call(env) end end end + + context "with root app" do + let(:app) do + Class.new do + def self.call(_) + :app_response + end + end + end + + let(:middleware) do + Class.new do + def self.call(_) + :middleware_response + end + end + end + + before do + root_app = app + middleware.define_singleton_method(:__rage_root_app) { root_app } + end + + it "delegates to root app" do + router.mount("/test", middleware, %w(GET)) + + result, _ = perform_get_request("/test") + expect(result).to eq(:app_response) + end + end end diff --git a/spec/support/integration_helper.rb b/spec/support/integration_helper.rb index f5f918f2..983db8a6 100644 --- a/spec/support/integration_helper.rb +++ b/spec/support/integration_helper.rb @@ -4,6 +4,7 @@ module IntegrationHelper def launch_server(env: {}) Bundler.with_unbundled_env do system("gem build -o rage-local.gem && gem install rage-local.gem --no-document") + system("gem install redis-client --no-document") if ENV["GITHUB_ACTIONS"] system("bundle install", chdir: "spec/integration/test_app") @pid = spawn(env, "bundle exec rage s", chdir: "spec/integration/test_app") sleep(2)