BigBinary Blog

We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.

Rails Routing -- a comprehensive look at routing

Following code was tested with edge rails (rails4) .

When a Rails application boots then it reads the config/routes.rb file. In your routes you might have code like this

1Rails4demo::Application.routes.draw do
2  root 'users#index'
3  resources :users
4  get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
5  get '/logout' => 'sessions#destroy', :as => :logout
6  get "/stories" => redirect("/photos")
7end

In the above case there are five different routing statements. Rails needs to store all those routes in a manner such that later when url is '/photos/5' then it should be able to find the right route statement that should handle the request.

In this article we are going to take a peek at how Rails handles the whole routing business.

Normalization in action

In order to compare various routing statements first all the routing statements need to be normalized to a standard format so that one can easily compare one route statement with another route statement.

Before we take a deep dive into how the normalization works lets first see some normalizations in action.

get call with defaults

Here we have following route

1Rails4demo::Application.routes.draw do
2  get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
3end

After the normalization process the above routing statement is transformed into five different variables. The values for all those five variables is shown below.

1app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fd05e0cf7e8
2           @defaults={:format=>"jpg", :controller=>"photos", :action=>"show"},
3           @glob_param=nil,
4           @controller_class_names=#<ThreadSafe::Cache:0x007fd05e0cf7c0
5           @backend={},
6           @default_proc=nil>>
7conditions: {:path_info=>"/photos/:id(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
8requirements: {}
9defaults: {:format=>"jpg", :controller=>"photos", :action=>"show"}
10as: nil
11anchor: true

app is the application that will be executed if conditions are met. conditions are the conditions. Pay attention to :path_info in conditions. This is used by Rails to determine the right route statement. defaults are defaults and requirements are the constraints.

GET call with as

Here we have following route

1Rails4demo::Application.routes.draw do
2  get '/logout' => 'sessions#destroy', :as => :logout
3end

After normalization above code gets following values

1app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f8ded87e740
2           @defaults={:controller=>"sessions", :action=>"destroy"},
3           @glob_param=nil,
4           @controller_class_names=#<ThreadSafe::Cache:0x007f8ded87e718 @backend={},
5           @default_proc=nil>>
6conditions: {:path_info=>"/logout(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
7requirements: {}
8defaults: {:controller=>"sessions", :action=>"destroy"}
9as: "logout"
10anchor: true

Notice that in the above case as is populate with logout .

root call

Here we have following route

1Rails4demo::Application.routes.draw do
2  root 'users#index'
3end

After normalization above code gets following values

1app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fe91507f278
2           @defaults={:controller=>"users", :action=>"index"},
3           @glob_param=nil,
4           @controller_class_names=#<ThreadSafe::Cache:0x007fe91507f250 @backend={},
5           @default_proc=nil>>
6conditions: {:path_info=>"/", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
7requirements: {}
8defaults: {:controller=>"users", :action=>"index"}
9as: "root"
10anchor: true

Notice that in the above case as is populated. And the path_info is / since this is the root url .

GET call with constraints

Here we have following route

1Rails4demo::Application.routes.draw do
2  #get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ }
3end

After normalization above code gets following values

1app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f8158e052c8
2           @defaults={:controller=>"pictures", :action=>"show"},
3           @glob_param=nil,
4           @controller_class_names=#<ThreadSafe::Cache:0x007f8158e05278 @backend={},
5           @default_proc=nil>>
6conditions: {:path_info=>"/pictures/:id(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
7requirements: {:id=>/[A-Z]\d{5}/}
8defaults: {:controller=>"pictures", :action=>"show"}
9as: nil
10anchor: true

Notice that in the above case requirements is populated with constraints mentioned in the route definition .

get with a redirect

Here we have following route

1Rails4demo::Application.routes.draw do
2  get "/stories" => redirect("/posts")
3end

After normalization above code gets following values

1app: redirect(301, /posts)
2conditions: {:path_info=>"/stories(.:format)", :required_defaults=>[], :request_method=>["GET"]}
3requirements: {}
4defaults: {}
5as: "stories"
6anchor: true

Notice that in the above case app is a simple redirect .

Resources

Here we have following route

1Rails4demo::Application.routes.draw do
2  resources :users
3end

After normalization above code gets following values

1app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a315c0
2           @defaults={:action=>"index", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a31598 @backend={}, @default_proc=nil>>
3conditions: {:path_info=>"/users(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
4defaults: {:action=>"index", :controller=>"users"}
5as: "users"
6
7app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a4ef80
8           @defaults={:action=>"create", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a4ef58 @backend={}, @default_proc=nil>>
9conditions: {:path_info=>"/users(.:format)", :required_defaults=>[:action, :controller], :request_method=>["POST"]}
10defaults: {:action=>"create", :controller=>"users"}
11as: nil
12
13app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41b63790
14           @defaults={:action=>"new", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41b63768 @backend={}, @default_proc=nil>>
15conditions: {:path_info=>"/users/new(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
16defaults: {:action=>"new", :controller=>"users"}
17as: "new_user"
18
19app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a10550
20           @defaults={:action=>"edit", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a10528 @backend={}, @default_proc=nil>>
21conditions: {:path_info=>"/users/:id/edit(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
22defaults: {:action=>"edit", :controller=>"users"}
23as: "edit_user"
24
25app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41f31818
26           @defaults={:action=>"show", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41f317f0 @backend={}, @default_proc=nil>>
27conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
28defaults: {:action=>"show", :controller=>"users"}
29as: "user"
30
31app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d44a9bb70
32           @defaults={:action=>"update", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d44a9bb48 @backend={}, @default_proc=nil>>
33conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["PATCH"]}
34defaults: {:action=>"update", :controller=>"users"}
35as: nil
36
37app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41b17480
38           @defaults={:action=>"update", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41b17458 @backend={}, @default_proc=nil>>
39conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["PUT"]}
40defaults: {:action=>"update", :controller=>"users"}
41as: nil
42
43app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d439ddf68
44           @defaults={:action=>"destroy", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d439ddf40 @backend={}, @default_proc=nil>>
45conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["DELETE"]}
46defaults: {:action=>"destroy", :controller=>"users"}
47as: nil

In this case I omitted requirements and anchor for brevity .

Notice that a single routing statement resources :users created eight normalized routing statements. It means that resources statement is basically a short cut for defining all those eight routing statements .

Resources with only

Here we have following route

1Rails4demo::Application.routes.draw do
2  resources :users, only: :new
3end

After normalization above code gets following values

1app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fdf55043e40
2           @defaults={:action=>"new", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007fdf55043e18 @backend={}, @default_proc=nil>>
3conditions: {:path_info=>"/users/new(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
4defaults: {:action=>"new", :controller=>"users"}
5as: "new_user"

Because of only keyword only one routing statement was produced in this case.

Mapper

In Rails ActionDispatch::Routing::Mapper class is responsible for normalizing all routing statements.

1module ActionDispatch
2  module Routing
3    class Mapper
4      include Base
5      include HttpHelpers
6      include Redirection
7      include Scoping
8      include Concerns
9      include Resources
10    end
11  end
12end

Now let's look at what these included modules do

Base

1module Base
2  def root (options = {})
3  end
4
5  def match
6  end
7
8  def mount(app, options = {})
9  end

As you can see Base handles root, match and mount calls.

HttpHelpers

1module HttpHelpers
2  def get(*args, &block)
3  end
4
5  def post(*args, &block)
6  end
7
8  def patch(*args, &block)
9  end
10
11  def put(*args, &block)
12  end
13
14  def delete(*args, &block)
15  end
16end

HttpHelpers handles get, post, patch, put and delete .

Scoping

1module Scoping
2  def scope(*args)
3  end
4
5  def namespace(path, options = {})
6  end
7
8  def constraints(constraints = {})
9  end
10end

Resources

1module Resources
2  def resource(*resources, &block)
3  end
4
5  def resources(*resources, &block)
6  end
7
8  def collection
9  end
10
11  def member
12  end
13
14  def shallow
15  end
16end

Let's put all the routes together

So now let's look at all the routes definition together.

1Rails4demo::Application.routes.draw do
2  root 'users#index'
3  get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
4  get '/logout' => 'sessions#destroy', :as => :logout
5  get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ }
6  get "/stories" => redirect("/posts")
7  resources :users
8end

Above routes definition produces following information. I am going to show info path info.

1{ :path_info=>"/":path_info=>"/photos/:id(.:format)" }
2
3{ :path_info=>"/logout(.:format)" }
4
5{ :path_info=>"/pictures/:id(.:format) }
6
7{ :path_info=>"/stories(.:format)" }
8
9{ :path_info=>"/users(.:format), :request_method=>["GET"]}
10
11{:path_info=>"/users(.:format)", :request_method=>["POST"]}
12
13{:path_info=>"/users/new(.:format)", :request_method=>["GET"]}
14
15{:path_info=>"/users/:id/edit(.:format)", :request_method=>["GET"]}
16
17{:path_info=>"/users/:id(.:format)", :controller], :request_method=>["GET"]}
18
19{:path_info=>"/users/:id(.:format)", :request_method=>["PATCH"]}
20
21{:path_info=>"/users/:id(.:format)", :request_method=>["PUT"]}
22
23{:path_info=>"/users/:id(.:format)", :request_method=>["DELETE"]}

How to find the matching route definition

So now that we have normalized the routing definitions the task at hand is to find the right route definition for the given url along with request_method.

For example if the requested page is /pictures/A12345 then the matching routing definition should be get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ } .

In order to accomplish that I would do something like this.

I would convert all path info into a regular expression and I would push that regular expression in an array. So in this case I would have 12 regular expressions in the array and for the given url I would try to match one by one.

This strategy will work and this is how Rails worked all the way up to Rails 3.1 .

Aaron Patterson loves computer science

Aaron Patterson noticed that finding the best matching route definition for a given url is nothing else but pattern matching task. And computer science solved this problem much more elegantly and this happens to run faster also by building an AST and walking over it.

So he decided to make a mini language out of the route definitions . After all the route definitions , we write , follow certain rules.

And thus Journey was born.

In the next blog we will see how to write grammar rules for routing definitions , how to parse and then walk the ast to see the best match .

Neeraj Singh in Rails
January 29, 2013
Share

Subscribe to our newsletter