Rails4 routes?
When a Rails application boots then it reads the config/routes.rb file. In your routes you might have code like this
Rails4demo::Application.routes.draw do
root 'users#index'
resources :users
get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
get '/logout' => 'sessions#destroy', :as => :logout
get "/stories" => redirect("/photos")
end
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
Rails4demo::Application.routes.draw do
get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
end
After the normalization process the above routing statement is transformeed into five different variables. The values for all those five varibles is shown below.
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fd05e0cf7e8
@defaults={:format=>"jpg", :controller=>"photos", :action=>"show"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007fd05e0cf7c0
@backend={},
@default_proc=nil>>
conditions: {:path_info=>"/photos/:id(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {}
defaults: {:format=>"jpg", :controller=>"photos", :action=>"show"}
as: nil
anchor: 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
Rails4demo::Application.routes.draw do
get '/logout' => 'sessions#destroy', :as => :logout
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f8ded87e740
@defaults={:controller=>"sessions", :action=>"destroy"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007f8ded87e718 @backend={},
@default_proc=nil>>
conditions: {:path_info=>"/logout(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {}
defaults: {:controller=>"sessions", :action=>"destroy"}
as: "logout"
anchor: true
Notice that in the above case as is populate with logout .
root call
Here we have following route
Rails4demo::Application.routes.draw do
root 'users#index'
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fe91507f278
@defaults={:controller=>"users", :action=>"index"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007fe91507f250 @backend={},
@default_proc=nil>>
conditions: {:path_info=>"/", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {}
defaults: {:controller=>"users", :action=>"index"}
as: "root"
anchor: 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
Rails4demo::Application.routes.draw do
#get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ }
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f8158e052c8
@defaults={:controller=>"pictures", :action=>"show"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007f8158e05278 @backend={},
@default_proc=nil>>
conditions: {:path_info=>"/pictures/:id(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {:id=>/[A-Z]\d{5}/}
defaults: {:controller=>"pictures", :action=>"show"}
as: nil
anchor: 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
Rails4demo::Application.routes.draw do
get "/stories" => redirect("/posts")
end
After normalization above code gets following values
app: redirect(301, /posts)
conditions: {:path_info=>"/stories(.:format)", :required_defaults=>[], :request_method=>["GET"]}
requirements: {}
defaults: {}
as: "stories"
anchor: true
Notice that in the above case app is a simple redirect .
resources
Here we have following route
Rails4demo::Application.routes.draw do
resources :users
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a315c0
@defaults={:action=>"index", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a31598 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"index", :controller=>"users"}
as: "users"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a4ef80
@defaults={:action=>"create", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a4ef58 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users(.:format)", :required_defaults=>[:action, :controller], :request_method=>["POST"]}
defaults: {:action=>"create", :controller=>"users"}
as: nil
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41b63790
@defaults={:action=>"new", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41b63768 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/new(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"new", :controller=>"users"}
as: "new_user"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a10550
@defaults={:action=>"edit", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a10528 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id/edit(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"edit", :controller=>"users"}
as: "edit_user"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41f31818
@defaults={:action=>"show", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41f317f0 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"show", :controller=>"users"}
as: "user"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d44a9bb70
@defaults={:action=>"update", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d44a9bb48 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["PATCH"]}
defaults: {:action=>"update", :controller=>"users"}
as: nil
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41b17480
@defaults={:action=>"update", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41b17458 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["PUT"]}
defaults: {:action=>"update", :controller=>"users"}
as: nil
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d439ddf68
@defaults={:action=>"destroy", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d439ddf40 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["DELETE"]}
defaults: {:action=>"destroy", :controller=>"users"}
as: 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
Rails4demo::Application.routes.draw do
resources :users, only: :new
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fdf55043e40
@defaults={:action=>"new", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007fdf55043e18 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/new(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"new", :controller=>"users"}
as: "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.
module ActionDispatch
module Routing
class Mapper
include Base
include HttpHelpers
include Redirection
include Scoping
include Concerns
include Resources
end
end
end
Now let's look at what these included modules do
Base
module Base
def root (options = {})
end
def match
end
def mount(app, options = {})
end
As you can see Base handles root, match and mount calls.
HttpHelpers
module HttpHelpers
def get(*args, &block)
end
def post(*args, &block)
end
def patch(*args, &block)
end
def put(*args, &block)
end
def delete(*args, &block)
end
end
HttpHelpers handles get, post, patch, put and delete .
Scoping
module Scoping
def scope(*args)
end
def namespace(path, options = {})
end
def constraints(constraints = {})
end
end
Resources
module Resources
def resource(*resources, &block)
end
def resources(*resources, &block)
end
def collection
end
def member
end
def shallow
end
end
Let's put all the routes together
So now let's look at all the routes definition together.
Rails4demo::Application.routes.draw do
root 'users#index'
get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
get '/logout' => 'sessions#destroy', :as => :logout
get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ }
get "/stories" => redirect("/posts")
resources :users
end
Above routes definition produces following information. I am going to show info path info.
{ :path_info=>"/":path_info=>"/photos/:id(.:format)" }
{ :path_info=>"/logout(.:format)" }
{ :path_info=>"/pictures/:id(.:format) }
{ :path_info=>"/stories(.:format)" }
{ :path_info=>"/users(.:format), :request_method=>["GET"]}
{:path_info=>"/users(.:format)", :request_method=>["POST"]}
{:path_info=>"/users/new(.:format)", :request_method=>["GET"]}
{:path_info=>"/users/:id/edit(.:format)", :request_method=>["GET"]}
{:path_info=>"/users/:id(.:format)", :controller], :request_method=>["GET"]}
{:path_info=>"/users/:id(.:format)", :request_method=>["PATCH"]}
{:path_info=>"/users/:id(.:format)", :request_method=>["PUT"]}
{: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 experssion 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 upto Rails 3.1 .
When a Rails application boots then it reads the config/routes.rb file. In your routes you might have code like this
Rails4demo::Application.routes.draw do
root 'users#index'
resources :users
get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
get '/logout' => 'sessions#destroy', :as => :logout
get "/stories" => redirect("/photos")
end
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
Rails4demo::Application.routes.draw do
get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
end
After the normalization process the above routing statement is transformeed into five different variables. The values for all those five varibles is shown below.
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fd05e0cf7e8
@defaults={:format=>"jpg", :controller=>"photos", :action=>"show"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007fd05e0cf7c0
@backend={},
@default_proc=nil>>
conditions: {:path_info=>"/photos/:id(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {}
defaults: {:format=>"jpg", :controller=>"photos", :action=>"show"}
as: nil
anchor: 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
Rails4demo::Application.routes.draw do
get '/logout' => 'sessions#destroy', :as => :logout
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f8ded87e740
@defaults={:controller=>"sessions", :action=>"destroy"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007f8ded87e718 @backend={},
@default_proc=nil>>
conditions: {:path_info=>"/logout(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {}
defaults: {:controller=>"sessions", :action=>"destroy"}
as: "logout"
anchor: true
Notice that in the above case as is populate with logout .
root call
Here we have following route
Rails4demo::Application.routes.draw do
root 'users#index'
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fe91507f278
@defaults={:controller=>"users", :action=>"index"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007fe91507f250 @backend={},
@default_proc=nil>>
conditions: {:path_info=>"/", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {}
defaults: {:controller=>"users", :action=>"index"}
as: "root"
anchor: 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
Rails4demo::Application.routes.draw do
#get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ }
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f8158e052c8
@defaults={:controller=>"pictures", :action=>"show"},
@glob_param=nil,
@controller_class_names=#<ThreadSafe::Cache:0x007f8158e05278 @backend={},
@default_proc=nil>>
conditions: {:path_info=>"/pictures/:id(.:format)", :required_defaults=>[:controller, :action], :request_method=>["GET"]}
requirements: {:id=>/[A-Z]\d{5}/}
defaults: {:controller=>"pictures", :action=>"show"}
as: nil
anchor: 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
Rails4demo::Application.routes.draw do
get "/stories" => redirect("/posts")
end
After normalization above code gets following values
app: redirect(301, /posts)
conditions: {:path_info=>"/stories(.:format)", :required_defaults=>[], :request_method=>["GET"]}
requirements: {}
defaults: {}
as: "stories"
anchor: true
Notice that in the above case app is a simple redirect .
resources
Here we have following route
Rails4demo::Application.routes.draw do
resources :users
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a315c0
@defaults={:action=>"index", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a31598 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"index", :controller=>"users"}
as: "users"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a4ef80
@defaults={:action=>"create", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a4ef58 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users(.:format)", :required_defaults=>[:action, :controller], :request_method=>["POST"]}
defaults: {:action=>"create", :controller=>"users"}
as: nil
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41b63790
@defaults={:action=>"new", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41b63768 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/new(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"new", :controller=>"users"}
as: "new_user"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41a10550
@defaults={:action=>"edit", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41a10528 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id/edit(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"edit", :controller=>"users"}
as: "edit_user"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41f31818
@defaults={:action=>"show", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41f317f0 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"show", :controller=>"users"}
as: "user"
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d44a9bb70
@defaults={:action=>"update", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d44a9bb48 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["PATCH"]}
defaults: {:action=>"update", :controller=>"users"}
as: nil
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d41b17480
@defaults={:action=>"update", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d41b17458 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["PUT"]}
defaults: {:action=>"update", :controller=>"users"}
as: nil
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007f9d439ddf68
@defaults={:action=>"destroy", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007f9d439ddf40 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/:id(.:format)", :required_defaults=>[:action, :controller], :request_method=>["DELETE"]}
defaults: {:action=>"destroy", :controller=>"users"}
as: 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
Rails4demo::Application.routes.draw do
resources :users, only: :new
end
After normalization above code gets following values
app: #<ActionDispatch::Routing::RouteSet::Dispatcher:0x007fdf55043e40
@defaults={:action=>"new", :controller=>"users"}, @glob_param=nil, @controller_class_names=#<ThreadSafe::Cache:0x007fdf55043e18 @backend={}, @default_proc=nil>>
conditions: {:path_info=>"/users/new(.:format)", :required_defaults=>[:action, :controller], :request_method=>["GET"]}
defaults: {:action=>"new", :controller=>"users"}
as: "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.
module ActionDispatch
module Routing
class Mapper
include Base
include HttpHelpers
include Redirection
include Scoping
include Concerns
include Resources
end
end
end
Now let's look at what these included modules do
Base
module Base
def root (options = {})
end
def match
end
def mount(app, options = {})
end
As you can see Base handles root, match and mount calls.
HttpHelpers
module HttpHelpers
def get(*args, &block)
end
def post(*args, &block)
end
def patch(*args, &block)
end
def put(*args, &block)
end
def delete(*args, &block)
end
end
HttpHelpers handles get, post, patch, put and delete .
Scoping
module Scoping
def scope(*args)
end
def namespace(path, options = {})
end
def constraints(constraints = {})
end
end
Resources
module Resources
def resource(*resources, &block)
end
def resources(*resources, &block)
end
def collection
end
def member
end
def shallow
end
end
Let's put all the routes together
So now let's look at all the routes definition together.
Rails4demo::Application.routes.draw do
root 'users#index'
get 'photos/:id' => 'photos#show', :defaults => { :format => 'jpg' }
get '/logout' => 'sessions#destroy', :as => :logout
get 'pictures/:id' => 'pictures#show', :constraints => { :id => /[A-Z]\d{5}/ }
get "/stories" => redirect("/posts")
resources :users
end
Above routes definition produces following information. I am going to show info path info.
{ :path_info=>"/":path_info=>"/photos/:id(.:format)" }
{ :path_info=>"/logout(.:format)" }
{ :path_info=>"/pictures/:id(.:format) }
{ :path_info=>"/stories(.:format)" }
{ :path_info=>"/users(.:format), :request_method=>["GET"]}
{:path_info=>"/users(.:format)", :request_method=>["POST"]}
{:path_info=>"/users/new(.:format)", :request_method=>["GET"]}
{:path_info=>"/users/:id/edit(.:format)", :request_method=>["GET"]}
{:path_info=>"/users/:id(.:format)", :controller], :request_method=>["GET"]}
{:path_info=>"/users/:id(.:format)", :request_method=>["PATCH"]}
{:path_info=>"/users/:id(.:format)", :request_method=>["PUT"]}
{: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 experssion 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 upto Rails 3.1 .
Comments
Post a Comment