Ruby redo’s: The Rails router

One of the things that Ruby is famous for is the ease with which you can build Domain Specific Languages (DSLs). The routing DSL in Rails is one of the more recognizable features of the framework and a good example of a Ruby DSL. If you’ve worked with Rails at all you have seen this in config/routes.rb:

MyApp::Application.routes.draw do
  match "/foo", to: "foo#bar"
end

I’ve worked with Rails a fair bit and had a pretty good understanding of how to use the DSL but there is always more to be learned by implementing it (or something like it) yourself. So what are we implementing? We want something that behaves like the ActionDispatch Routeset:

[27] pry(main)> Rails.application.routes
=> #<ActionDispatch::Routing::RouteSet:0x0000000232c588>
[28] pry(main)> Rails.application.routes.draw do
[28] pry(main)*   match "/foo", to: "bar#baz", via: :get
[28] pry(main)* end
=> nil

So we can see that it has a draw method that accepts a block. That block contains a call to the method “match” and accepts a string and a hash of arguments. We called the match method in our code block above and passed that block to the draw methods. When match is evaluated it adds the given route to the set of routes for the application. Which means that if we dig into the routes we should find our path we specified (/foo). Sure enough:

[29] pry(main)> Rails.application.routes.router.routes.each{|r| puts r.path.spec};nil;
/assets
/foo(.:format)
/rails/info/properties(.:format)

As with most things in Ruby, its actually surprisingly little code to get such a thing working:

class RouteSet

  def initialize(routes = {})
    @routes = routes
  end

  def match(path, options)
    @routes[path]= options
  end

  def draw(&block)
    instance_eval &block
  end

  def to_s
    puts @routes.inspect
  end

end

We can play with it in Pry:

mike@sleepycat:~/projects/play$ pry -I.
[1] pry(main)> load 'routes.rb'
=> true
[2] pry(main)> routeset = RouteSet.new
{}
=> #<RouteSet:0x1561598>
[3] pry(main)> routeset.draw do
[3] pry(main)*   match "/foo", to: "bar#baz", via: :get
[3] pry(main)* end
=> {:to=>"bar#baz", :via=>:get}
[4] pry(main)> routeset.to_s
{"/foo"=>{:to=>"bar#baz", :via=>:get}}
=> nil

The secret DSL sauce is all in the draw method. Notice the ampersand in front of the block parameter:

  def draw(&block)
    instance_eval &block
  end

That ampersand operator wraps an incoming block in a Proc and then binds it to a local variable named block. The same operator is used to reverse that process, turning the contents of the block variable from a Proc back into a block. That block is then fed into instance_eval which evaluates the block in the context of the current object. The net effect is the same as if you had just written this:

  def draw
    match "/foo", to: "bar#baz", via: :get
  end

This process of taking a block of code defined somewhere and evaluating it in some other context is the key to DSLs in Ruby. Understanding the ampersand operator and its conversion between blocks and Procs is really important since this is really common in Ruby code. While is may be common, its not cheap. All that “binding to variables” stuff can be slow so in those moments where you care about speed you will want to use this instead:

  def draw
    instance_eval &Proc.new
  end

Playing with this stuff has really helped my understanding of both Ruby and Rails. I hope it helps you too.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s