From ef91cddb48d1fa8d1a34e8e5ac68fe9eb56c160f Mon Sep 17 00:00:00 2001 From: "@schneems and @mattt" Date: Sun, 1 Jul 2012 20:00:10 -0700 Subject: [PATCH] move route_inspector to actionpack this is so we can show route output in the development when we get a routing error. Railties can use features of ActionDispatch, but ActionDispatch should not depend on Railties. --- .../middleware/debug_exceptions.rb | 12 +- .../lib/action_dispatch/routing/inspector.rb | 121 +++++++++++++ .../test/dispatch/routing/inspector_test.rb | 170 ++++++++++++++++++ railties/lib/rails/application.rb | 4 +- railties/lib/rails/info_controller.rb | 3 +- railties/lib/rails/tasks/routes.rake | 4 +- 6 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 actionpack/lib/action_dispatch/routing/inspector.rb create mode 100644 actionpack/test/dispatch/routing/inspector_test.rb diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 467437b512..af3e6b3557 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -1,6 +1,6 @@ require 'action_dispatch/http/request' require 'action_dispatch/middleware/exception_wrapper' -require 'rails/application/route_inspector' +require 'action_dispatch/routing/inspector' module ActionDispatch @@ -9,8 +9,9 @@ module ActionDispatch class DebugExceptions RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') - def initialize(app) - @app = app + def initialize(app, routes_app = nil) + @app = app + @routes_app = routes_app end def call(env) @@ -84,9 +85,10 @@ module ActionDispatch private def formatted_routes(exception) + return false unless @routes_app.respond_to?(:routes) if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error) - inspector = Rails::Application::RouteInspector.new - inspector.format(Rails.application.routes.routes).join("\n") + inspector = ActionDispatch::Routing::RouteInspector.new + inspector.format(@routes_app.routes.routes).join("\n") end end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb new file mode 100644 index 0000000000..17e19453ea --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -0,0 +1,121 @@ +require 'delegate' + +module ActionDispatch + module Routing + class RouteWrapper < SimpleDelegator + def endpoint + rack_app ? rack_app.inspect : "#{controller}##{action}" + end + + def constraints + requirements.except(:controller, :action) + end + + def rack_app(app = self.app) + @rack_app ||= begin + class_name = app.class.name.to_s + if class_name == "ActionDispatch::Routing::Mapper::Constraints" + rack_app(app.app) + elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/ + app + end + end + end + + def verb + super.source.gsub(/[$^]/, '') + end + + def path + super.spec.to_s + end + + def name + super.to_s + end + + def reqs + @reqs ||= begin + reqs = endpoint + reqs += " #{constraints.inspect}" unless constraints.empty? + reqs + end + end + + def controller + requirements[:controller] || ':controller' + end + + def action + requirements[:action] || ':action' + end + + def internal? + path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}} + end + + def engine? + rack_app && rack_app.respond_to?(:routes) + end + end + + ## + # This class is just used for displaying route information when someone + # executes `rake routes`. People should not use this class. + class RouteInspector # :nodoc: + def initialize + @engines = Hash.new + end + + def format(all_routes, filter = nil) + if filter + all_routes = all_routes.select{ |route| route.defaults[:controller] == filter } + end + + routes = collect_routes(all_routes) + + formatted_routes(routes) + + formatted_routes_for_engines + end + + def collect_routes(routes) + routes = routes.collect do |route| + RouteWrapper.new(route) + end.reject do |route| + route.internal? + end.collect do |route| + collect_engine_routes(route) + + {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs } + end + end + + 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) + end + end + + def formatted_routes_for_engines + @engines.map do |name, routes| + ["\nRoutes for #{name}:"] + formatted_routes(routes) + end.flatten + end + + def formatted_routes(routes) + name_width = routes.map{ |r| r[:name].length }.max + verb_width = routes.map{ |r| r[:verb].length }.max + path_width = routes.map{ |r| r[:path].length }.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 + end +end diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb new file mode 100644 index 0000000000..ae830300a1 --- /dev/null +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -0,0 +1,170 @@ +require 'minitest/autorun' +require 'action_controller' +require 'rails/engine' +require 'action_dispatch/routing/inspector' + +module ActionDispatch + module Routing + class RouteInspectTest < ActiveSupport::TestCase + def setup + @set = ActionDispatch::Routing::RouteSet.new + @inspector = ActionDispatch::Routing::RouteInspector.new + app = ActiveSupport::OrderedOptions.new + app.config = ActiveSupport::OrderedOptions.new + app.config.assets = ActiveSupport::OrderedOptions.new + app.config.assets.prefix = '/sprockets' + Rails.stubs(:application).returns(app) + Rails.stubs(:env).returns("development") + end + + def draw(&block) + @set.draw(&block) + @inspector.format(@set.routes) + end + + def test_displaying_routes_for_engines + engine = Class.new(Rails::Engine) do + def self.to_s + "Blog::Engine" + end + end + engine.routes.draw do + get '/cart', :to => 'cart#show' + end + + output = draw do + get '/custom/assets', :to => 'custom_assets#show' + mount engine => "/blog", :as => "blog" + end + + expected = [ + "custom_assets GET /custom/assets(.:format) custom_assets#show", + " blog /blog Blog::Engine", + "\nRoutes for Blog::Engine:", + "cart GET /cart(.:format) cart#show" + ] + assert_equal expected, output + end + + def test_cart_inspect + output = draw do + get '/cart', :to => 'cart#show' + end + assert_equal ["cart GET /cart(.:format) cart#show"], output + end + + def test_inspect_shows_custom_assets + output = draw do + get '/custom/assets', :to => 'custom_assets#show' + end + assert_equal ["custom_assets GET /custom/assets(.:format) custom_assets#show"], output + end + + def test_inspect_routes_shows_resources_route + output = draw do + resources :articles + end + expected = [ + " articles GET /articles(.:format) articles#index", + " POST /articles(.:format) articles#create", + " new_article GET /articles/new(.:format) articles#new", + "edit_article GET /articles/:id/edit(.:format) articles#edit", + " article GET /articles/:id(.:format) articles#show", + " PATCH /articles/:id(.:format) articles#update", + " PUT /articles/:id(.:format) articles#update", + " DELETE /articles/:id(.:format) articles#destroy" ] + assert_equal expected, output + end + + def test_inspect_routes_shows_root_route + output = draw do + root :to => 'pages#main' + end + assert_equal ["root GET / pages#main"], output + end + + def test_inspect_routes_shows_dynamic_action_route + output = draw do + get 'api/:action' => 'api' + end + assert_equal [" GET /api/:action(.:format) api#:action"], output + end + + def test_inspect_routes_shows_controller_and_action_only_route + output = draw do + get ':controller/:action' + end + assert_equal [" GET /:controller/:action(.:format) :controller#:action"], output + end + + def test_inspect_routes_shows_controller_and_action_route_with_constraints + output = draw do + get ':controller(/:action(/:id))', :id => /\d+/ + end + assert_equal [" GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"], output + end + + def test_rake_routes_shows_route_with_defaults + output = draw do + get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'} + end + assert_equal [%Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]], output + end + + def test_rake_routes_shows_route_with_constraints + output = draw do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + assert_equal [" GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"], output + end + + class RackApp + def self.call(env) + end + end + + def test_rake_routes_shows_route_with_rack_app + output = draw do + get 'foo/:id' => RackApp, :id => /[A-Z]\d{5}/ + end + assert_equal [" GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"], output + end + + def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints + constraint = Class.new do + def to_s + "( my custom constraint )" + end + end + + output = draw do + scope :constraint => constraint.new do + mount RackApp => '/foo' + end + end + + assert_equal [" /foo #{RackApp.name} {:constraint=>( my custom constraint )}"], output + end + + def test_rake_routes_dont_show_app_mounted_in_assets_prefix + output = draw do + get '/sprockets' => RackApp + end + assert_no_match(/RackApp/, output.first) + assert_no_match(/\/sprockets/, output.first) + end + + def test_redirect + output = draw do + get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" } + get "/bar" => redirect(path: "/foo/bar", status: 307) + get "/foobar" => redirect{ "/foo/bar" } + end + + assert_equal " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}", output[0] + assert_equal " bar GET /bar(.:format) redirect(307, path: /foo/bar)", output[1] + assert_equal "foobar GET /foobar(.:format) redirect(301)", output[2] + end + end + end +end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index d5ec2cbfd9..3afbf0a03e 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -267,6 +267,7 @@ module Rails def default_middleware_stack #:nodoc: ActionDispatch::MiddlewareStack.new.tap do |middleware| + app = self if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache require "action_dispatch/http/rack_cache" middleware.use ::Rack::Cache, rack_cache @@ -290,11 +291,10 @@ module Rails middleware.use ::ActionDispatch::RequestId middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path) - middleware.use ::ActionDispatch::DebugExceptions + middleware.use ::ActionDispatch::DebugExceptions, app middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies unless config.cache_classes - app = self middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? } end diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index bacdcbf3aa..83ab8c7e9d 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -1,4 +1,4 @@ -require 'rails/application/routes_inspector' +require 'action_dispatch/routing/inspector' class Rails::InfoController < ActionController::Base self.view_paths = File.join(File.dirname(__FILE__), 'templates') @@ -16,6 +16,7 @@ class Rails::InfoController < ActionController::Base def routes inspector = Rails::Application::RoutesInspector.new + inspector = ActionDispatch::Routing::RouteInspector.new @info = inspector.format(_routes.routes).join("\n") end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index 4ade825616..afc4e147e1 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -1,7 +1,7 @@ desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.' task :routes => :environment do all_routes = Rails.application.routes.routes - require 'rails/application/routes_inspector' - inspector = Rails::Application::RoutesInspector.new + require 'action_dispatch/routing/inspector' + inspector = ActionDispatch::Routing::RouteInspector.new puts inspector.format(all_routes, ENV['CONTROLLER']).join "\n" end