Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

In Browser Path Matching with Javascript #9001

Merged
merged 1 commit into from
@schneems
Collaborator

When debugging routes it can sometimes be difficult to understand exactly how the paths are matched. This PR adds a JS based path matching widget to the /rails/info/routes output. You can enter in a path, and it will tell you which of the routes that path matches, while preserving order (top match wins).

The matching widget in action:

Prior to this PR the only way to check matching paths is via mental math, or typing in a path in the url bar and seeing where it goes. This feature will be an invaluable debugging tool by dramatically decreasing the time needed to check a path match.

To newcomers: you can run this route page in Rails 3.2 (without this feature), using Sextant.

It has also been noted that this PR is: OVER 9000.

ATP actionpack

@mattdbridges

Wow. Just Wow. :+1:

@Jauny

yea just wow, amazing :+1:

@frodsan

Awesome :+1:

@MadRabbit

$ rails routes | grep foo

but generally looks like fun :)

..._dispatch/middleware/templates/routes/_table.html.erb
@@ -52,5 +88,60 @@
});
}
+ // takes an array of elements with a data-regex attribute and
+ // passes their their parent <tr> into the callback function
+ // if the regex matchs a given path
+ function eachElemsForPath(elems, path, func) {
+ each(elems, function(e){
+ var reg = e.getAttribute("data-regex");
+ if (path.match(RegExp(reg))) {
+ func(e.parentNode.cloneNode(true));
+ }
+ })
+ }
+
+ // Ensure path always starts with a slash "/" and remove params or fragments
+ function sanatizePath(path) {
@h3h
h3h added a note

sanitizePath for the greater spelling win. :smile:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Jacke

:+1: Awesome!!

@drnic

A very appropriate PR for #9001!

@aviflombaum

dude, killer. :+1: its like better_errors for routes :-)

@jrgifford

does this work in production? That's the only question I have. (i will admit i haven't looked at the code for this.)

@schneems
Collaborator

@jrgifford you cannot access this url in production, it is in an internal controller only added in development, and there is a before filter that will prevent anyone from accessing this page unless it is a local request.

@shedd

Fantastic idea!

@hankblinq

Clap Clap Clap!

@jrgifford

@schneems ah, ok. excellent!

@thenickcox

Here's to hoping we use the ability to use animated GIFs in pull requests for good, not for annoying.

@codeblooded

:+1: Yeah!!!

@jgaskins

@drnic This PR is, indeed, over 9000.

actionpack/lib/action_dispatch/routing/inspector.rb
@@ -3,6 +3,12 @@
module ActionDispatch
module Routing
class RouteWrapper < SimpleDelegator
+
+ def initialize(route)
+ @route = route
+ super(@route)
+ end
+
@pixeltrix Owner

I assume that you're capturing the route for the regex method below? If so can't we delete this method and implement regex like this:

def regex
  __get_object__.path.to_regexp
end

Also can we keep the names consistent - use regexp instead of regex.

@schneems Collaborator

thanks, i had never worked with SimpleDelegator before, looks like it's __getobj__

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
..._dispatch/middleware/templates/routes/_table.html.erb
@@ -1,22 +1,58 @@
<% content_for :style do %>
- #route_table td { padding: 0 30px; }
- #route_table { margin: 0 auto 0; }
+ #route_table td {
+ padding: 0 30px;
+ }
+
+ #route_table {
+ margin: 0 auto 0;

two spaces after :. Also move this #route_table definition above the other one (as the first one).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
..._dispatch/middleware/templates/routes/_table.html.erb
((8 lines not shown))
+ #route_table {
+ margin: 0 auto 0;
+ border-collapse: collapse;
+ }
+
+ #route_table tr.bottom th {
+ padding-bottom: 10px;
+ line-height: 15px;
+ }
+
+ #route_table .matched_paths {
+ background-color: LightGoldenRodYellow ;
+ }
+
+ #route_table .matched_paths {
+ border-bottom: solid 3px SlateGrey ;

Wrong indent, space before ;.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
..._dispatch/middleware/templates/routes/_table.html.erb
((4 lines not shown))
+ #route_table td {
+ padding: 0 30px;
+ }
+
+ #route_table {
+ margin: 0 auto 0;
+ border-collapse: collapse;
+ }
+
+ #route_table tr.bottom th {
+ padding-bottom: 10px;
+ line-height: 15px;
+ }
+
+ #route_table .matched_paths {
+ background-color: LightGoldenRodYellow ;

Wrong indent, space before ;.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carlosantoniodasilva carlosantoniodasilva commented on the diff
..._dispatch/middleware/templates/routes/_table.html.erb
((59 lines not shown))
</tr>
</thead>
+ <tbody class='matched_paths' id='matched_paths'>
+ </tbody>

Is it allowed to have as many tbody as we want? Just wondering...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
..._dispatch/middleware/templates/routes/_table.html.erb
((10 lines not shown))
+ if (path.match(RegExp(reg))) {
+ func(e.parentNode.cloneNode(true));
+ }
+ })
+ }
+
+ // Ensure path always starts with a slash "/" and remove params or fragments
+ function sanitizePath(path) {
+ var path = path.charAt(0) == '/' ? path : "/" + path
+ return path.replace(/\#.*|\?.*/, '')
+ }
+
+ // Enables path search functionality
+ function setupMatchPaths() {
+ var regex_elems = document.querySelectorAll('#route_table [data-regex]');
+ var path_elem = document.querySelector('#path_search')

No ; at the end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
..._dispatch/middleware/templates/routes/_table.html.erb
((12 lines not shown))
+ }
+ })
+ }
+
+ // Ensure path always starts with a slash "/" and remove params or fragments
+ function sanitizePath(path) {
+ var path = path.charAt(0) == '/' ? path : "/" + path
+ return path.replace(/\#.*|\?.*/, '')
+ }
+
+ // Enables path search functionality
+ function setupMatchPaths() {
+ var regex_elems = document.querySelectorAll('#route_table [data-regex]');
+ var path_elem = document.querySelector('#path_search')
+ var selected_section = document.querySelector('#matched_paths');
+ var no_match_text = '<tr><th colspan="4">None</th></tr>';

Actually you could use one var declaration only.

And change variables names to be camelCase.

@schneems Collaborator

It's not often that I get the opportunity to refactor javascript code... can you give me more information by what you mean by only using one var declaration?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@schneems
Collaborator

Updated, please review latest code.

@steveklabnik
Collaborator

Can't be auto-merged

@schneems
Collaborator

CHANGELOG!!!!!!!!!!!!!! (said Star Trek II style). I pushed an update. I wish GH would make the merge-ability of a PR public info.

@agis-

Oh my this is awesome!

@vendethiel

This is over nine thousands.

..._dispatch/middleware/templates/routes/_table.html.erb
@@ -25,7 +61,7 @@
<script type='text/javascript'>
function each(elems, func) {
if (!elems instanceof Array) { elems = [elems]; }
- for (var i = elems.length; i--; ) {
+ for (var i = 0; i < elems.length; i++) {

you'd like to cache it for best performances, ie for (var i = 0, len = elems.length; i < len; i++)

@schneems Collaborator

thanks @Nami-Doc, updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@samqiu

You are awesome!

actionpack/CHANGELOG.md
@@ -1,5 +1,10 @@
## Rails 4.0.0 (unreleased) ##
+* Add javascript based routing path matcher to `/rails/info/routes`.
+ Routes can now be filtered by whether or not they match a path.
+
+ Richard Schneeman
@rafaelfranca Owner
*Richard Schneeman*
@schneems Collaborator

Fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
actionpack/lib/action_dispatch/routing/inspector.rb
@@ -34,6 +34,14 @@ def name
super.to_s
end
+ def regexp
+ __getobj__.path.to_regexp
+ end
+
+ def json_regexp
+ Regexp.new(regexp.inspect.sub('\\A','^').sub('\\Z','$').sub('\\z','$').sub(/^\//,'').sub(/\/[a-z]*$/,'').gsub(/\(\?#.+\)/, '').gsub(/\(\?-\w+:/,'(').gsub(/\s/,'')).source
@rafaelfranca Owner

Would be good to break this line and also use spaces before after the ,

@schneems Collaborator

updated and using a var for clarity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@schneems schneems In Browser Path Matching with Javascript
When debugging routes ,it can sometimes be difficult to understand exactly how the paths are matched. This PR adds a JS based path matching widget to the `/rails/info/routes` output. You can enter in a path, and it will tell you which of the routes that path matches, while preserving order (top match wins).

The matching widget in action:

![](http://f.cl.ly/items/3A2F0v2m3m1Z1p3P3O3k/path-match.gif)

Prior to this PR the only way to check matching paths is via mental math, or typing in a path in the url bar and seeing where it goes. This feature will be an invaluable debugging tool by dramatically decreasing the time needed to check a path match. 

ATP actionpack
8b72d68
@rafaelfranca rafaelfranca merged commit 68a6fb6 into from
@calebthompson

@schneems What did you use to make the gif?

@kuraga

:-1: I think info pages should provide minimal information only.

I think this is good.
Right now I am just using rake routes and it is slow

@kuraga - little things like this go a long way to keep developers happy. Happy developers are what keep the framework thriving.
It would be nice if the inline javascripts and stylesheets are cleaned up to external assets as it seems like a long step backwards, but this looks to match the rest of templates implementations.
@schneems - thanks for your work. This is great stuff.

To get this feature on non Rails 4 versions, install schneem's gem, Sextant

@brandonblack

very nice

@flexoid

That's just awesome! :thumbsup:

@miguelff

Awesome! :heart:

@pedrosnk

lovely

@bsodmike

Dude @schneems :heart: .... :+1: :thumbsup:

@ravidsrk

Awesome stuff <3

@alanmeira

awesome! :heart_eyes:

@wesleychang

Awesome!!!

@camsong

Amazing :+1:

@wizztjh

+1

@jwo jwo referenced this pull request in mperham/sidekiq
Closed

Matrix Style Log Formatter #646

@hujinpu

Nice feature!

@jankeesvw

Nice work! :+1:

@ahmet

Nice :thumbsup:

@josin

Good job! :+1:

@passion8

very good job,thanks
:+1:

@peppyheppy

:+1: I like. (the gif and the PR)

@FUT

great!

@karthie

amazed

@tubbo

love it, but what happens if i want to do rails g resource rails? i wanna be meta!!1 ;-) :shipit:

@rahul100885

Just awesome

@krishnasrihari

Awesome

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 21, 2013
  1. @schneems

    In Browser Path Matching with Javascript

    schneems authored
    When debugging routes ,it can sometimes be difficult to understand exactly how the paths are matched. This PR adds a JS based path matching widget to the `/rails/info/routes` output. You can enter in a path, and it will tell you which of the routes that path matches, while preserving order (top match wins).
    
    The matching widget in action:
    
    ![](http://f.cl.ly/items/3A2F0v2m3m1Z1p3P3O3k/path-match.gif)
    
    Prior to this PR the only way to check matching paths is via mental math, or typing in a path in the url bar and seeing where it goes. This feature will be an invaluable debugging tool by dramatically decreasing the time needed to check a path match. 
    
    ATP actionpack
This page is out of date. Refresh to see the latest.
View
5 actionpack/CHANGELOG.md
@@ -1,5 +1,10 @@
## Rails 4.0.0 (unreleased) ##
+* Add javascript based routing path matcher to `/rails/info/routes`.
+ Routes can now be filtered by whether or not they match a path.
+
+ *Richard Schneeman*
+
* Given
params.permit(:name)
View
2  actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
@@ -7,7 +7,7 @@
<td data-route-verb='<%= route[:verb] %>'>
<%= route[:verb] %>
</td>
- <td data-route-path='<%= route[:path] %>'>
+ <td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'>
<%= route[:path] %>
</td>
<td data-route-reqs='<%= route[:reqs] %>'>
View
108 actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -1,22 +1,58 @@
<% content_for :style do %>
- #route_table td { padding: 0 30px; }
- #route_table { margin: 0 auto 0; }
+ #route_table {
+ margin: 0 auto 0;
+ border-collapse: collapse;
+ }
+
+ #route_table td {
+ padding: 0 30px;
+ }
+
+ #route_table tr.bottom th {
+ padding-bottom: 10px;
+ line-height: 15px;
+ }
+
+ #route_table .matched_paths {
+ background-color: LightGoldenRodYellow;
+ }
+
+ #route_table .matched_paths {
+ border-bottom: solid 3px SlateGrey;
+ }
+
+ #path_search {
+ width: 80%;
+ font-size: inherit;
+ }
<% end %>
<table id='route_table' class='route_table'>
<thead>
<tr>
- <th>Helper<br />
+ <th>Helper</th>
+ <th>HTTP Verb</th>
+ <th>Path</th>
+ <th>Controller#Action</th>
+ </tr>
+ <tr class='bottom'>
+ <th><%# Helper %>
<%= link_to "Path", "#", 'data-route-helper' => '_path',
title: "Returns a relative path (without the http or domain)" %> /
<%= link_to "Url", "#", 'data-route-helper' => '_url',
- title: "Returns an absolute url (with the http and domain)" %>
+ title: "Returns an absolute url (with the http and domain)" %>
+ </th>
+ <th><%# HTTP Verb %>
+ </th>
+ <th><%# Path %>
+ <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %>
+ </th>
+ <th><%# Controller#action %>
</th>
- <th>HTTP Verb</th>
- <th>Path</th>
- <th>Controller#Action</th>
</tr>
</thead>
+ <tbody class='matched_paths' id='matched_paths'>
+ </tbody>

Is it allowed to have as many tbody as we want? Just wondering...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
<tbody>
<%= yield %>
</tbody>
@@ -25,7 +61,7 @@
<script type='text/javascript'>
function each(elems, func) {
if (!elems instanceof Array) { elems = [elems]; }
- for (var i = elems.length; i--; ) {
+ for (var i = 0, len = elems.length; i < len; i++) {
func(elems[i]);
}
}
@@ -46,11 +82,63 @@
function setupRouteToggleHelperLinks() {
var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
onClick(toggleLinks, function(){
- var helperTxt = this.getAttribute("data-route-helper");
- var helperElems = document.querySelectorAll('[data-route-name] span.helper');
+ var helperTxt = this.getAttribute("data-route-helper"),
+ helperElems = document.querySelectorAll('[data-route-name] span.helper');
setValOn(helperElems, helperTxt);
});
}
+ // takes an array of elements with a data-regexp attribute and
+ // passes their their parent <tr> into the callback function
+ // if the regexp matchs a given path
+ function eachElemsForPath(elems, path, func) {
+ each(elems, function(e){
+ var reg = e.getAttribute("data-regexp");
+ if (path.match(RegExp(reg))) {
+ func(e.parentNode.cloneNode(true));
+ }
+ })
+ }
+
+ // Ensure path always starts with a slash "/" and remove params or fragments
+ function sanitizePath(path) {
+ var path = path.charAt(0) == '/' ? path : "/" + path;
+ return path.replace(/\#.*|\?.*/, '');
+ }
+
+ // Enables path search functionality
+ function setupMatchPaths() {
+ var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
+ pathElem = document.querySelector('#path_search'),
+ selectedSection = document.querySelector('#matched_paths'),
+ noMatchText = '<tr><th colspan="4">None</th></tr>';
+
+
+ // Remove matches if no path is present
+ pathElem.onblur = function(e) {
+ if (pathElem.value === "") selectedSection.innerHTML = "";
+ }
+
+ // On key press perform a search for matching paths
+ pathElem.onkeyup = function(e){
+ var path = sanitizePath(pathElem.value),
+ defaultText = '<tr><th colspan="4">Paths Matching (' + path + '):</th></tr>';
+
+ // Clear out results section
+ selectedSection.innerHTML= defaultText;
+
+ // Display matches if they exist
+ eachElemsForPath(regexpElems, path, function(e){
+ selectedSection.appendChild(e);
+ });
+
+ // If no match present, tell the user
+ if (selectedSection.innerHTML === defaultText) {
+ selectedSection.innerHTML = selectedSection.innerHTML + noMatchText;
+ }
+ }
+ }
+
+ setupMatchPaths();
setupRouteToggleHelperLinks();
</script>
View
23 actionpack/lib/action_dispatch/routing/inspector.rb
@@ -34,6 +34,23 @@ def name
super.to_s
end
+ def regexp
+ __getobj__.path.to_regexp
+ end
+
+ def json_regexp
+ str = regexp.inspect.
+ sub('\\A' , '^').
+ sub('\\Z' , '$').
+ sub('\\z' , '$').
+ sub(/^\// , '').
+ sub(/\/[a-z]*$/ , '').
+ gsub(/\(\?#.+\)/ , '').
+ gsub(/\(\?-\w+:/ , '(').
+ gsub(/\s/ , '')
+ Regexp.new(str).source
+ end
+
def reqs
@reqs ||= begin
reqs = endpoint
@@ -101,7 +118,11 @@ def collect_routes(routes)
end.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,
+ regexp: route.json_regexp }
end
end
View
8 actionpack/test/dispatch/routing/inspector_test.rb
@@ -21,6 +21,14 @@ def draw(options = {}, &block)
inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n")
end
+ def test_json_regexp_converter
+ @set.draw do
+ get '/cart', :to => 'cart#show'
+ end
+ route = ActionDispatch::Routing::RouteWrapper.new(@set.routes.first)
+ assert_equal "^\\/cart(?:\\.([^\\/.?]+))?$", route.json_regexp
+ end
+
def test_displaying_routes_for_engines
engine = Class.new(Rails::Engine) do
def self.inspect
Something went wrong with that request. Please try again.