Digging Deeper: Router Internals
In this section, we'll explore how Rails implements the beautiful routing DSL behind the scenes. This is going to be a bit technical, but nothing too complex, I promise.
Let's revisit the config/routes.rb
file we saw earlier.
When you create a new Rails app, it automatically creates a /routes.rb
file in the config
directory. All it contains is a single call to the draw
method on the object returned by the Rails.application.routes
method. Additionally, it takes a block containing the routes for your application.
Rails.application.routes.draw do
# application routes go here...
end
To understand how the above method works, we need to take a short detour and understand Ruby's instance_exec
method defined on the BasicObject
class.
Understanding How instance_exec
Works
The instance_exec
method takes a block and runs that block in the context of the object on which it's called. Inside the block, it sets self
to that object, so you can access its instance variables and methods.
class Company
def initialize(name, product)
@name = name
@product = product
end
def info
"#{@name}: #{@product}"
end
end
microsoft = Company.new 'Microsoft', 'Excel'
microsoft.instance_exec do
puts info # access instance method
puts @name # access instance variable
end
# Output:
# Microsoft: Excel
# Microsoft
You might wonder what's the purpose of using instance_exec
when you can just call the method directly on microsoft
. You are, of course, right in this example, but, as we'll soon see, the most important benefit of instance_exec
method is that it allows you to pass a block and run it later in the context of an object. This is really useful technique for creating special-purpose DSLs.
Not sure what I mean? let's revisit the routes.rb
file.
Rails.application.routes.draw do
end
The Rails.application
is an instance of the Application
class which inherits from the Engine
class. The Engine
class has a routes
method which returns an instance of the RouteSet
class.
# railties/lib/rails/engine.rb
def routes(&block)
@routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)
# ...
@routes
end
# Define the configuration object for the engine.
def config
@config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from))
end
It means that the block passed to the draw
method in the routes.rb
file is ultimately received by the draw
method on an instance of the RouteSet
class. This method passes that block to the eval_block
method, as seen below.
# actionpack/lib/action_dispatch/routing/route_set.rb
def draw(&block)
# ...
eval_block(block)
# ...
end
def eval_block(block)
mapper = Mapper.new(self)
mapper.instance_exec(&block)
end
As you can see, the eval_block
method first creates an instance of the ActionDispatch::Routing::Mapper
class and executes the block within the context of that instance.
What it means, is that any code we write inside the block passed to the Rails.application.routes.draw
method will be evaluated as if it was written inside the Mapper
class.
If this is already started to get overwhelming and you feel like giving up, DON'T. Clone the Rails repo, open it in the editor, and trace the call stack with me. It's a fun exercise and you'll learn a lot along the way. If you want to learn how to read the Rails codebase without getting overwhelmed, check out: How I Read Rails Source Code
For example, the two pieces of code below are similar. However, the first version just reads better. It feels like a programming language specifically designed for the routing domain.
Rails.application.routes.draw do
root 'application#index'
get 'posts/publish', to: 'posts#publish'
end
# is similar to
routes = ActionDispatch::Routing::RouteSet.new
mapper = Mapper.new(routes)
mapper.root 'application#index'
mapper.get 'posts/publish', to: 'posts#publish'
This also means that whenever you see a method in your routes
file, you know where to look for its definition. It's the ActionDispatch::Routing::Mapper
class and its included modules. For example, the commonly used get
method is defined in the Mapper::HttpHelpers
module.
def get(*args, &block)
map_method(:get, args, &block)
end
This gives us a solid base to explore the Rails Routing API. We'll start our investigation with the match
method.
If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. You can also subscribe to my blog to receive future posts via email.