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
This commit is contained in:
schneems 2013-01-18 15:18:23 -06:00
parent 2e8b3d5c9a
commit 8b72d689e3
5 changed files with 135 additions and 13 deletions

@ -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)

@ -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] %>'>

@ -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)" %>
</th>
<th>HTTP Verb</th>
<th>Path</th>
<th>Controller#Action</th>
<th><%# HTTP Verb %>
</th>
<th><%# Path %>
<%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %>
</th>
<th><%# Controller#action %>
</th>
</tr>
</thead>
<tbody class='matched_paths' id='matched_paths'>
</tbody>
<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>

@ -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

@ -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