Master Sword
<%= 'Cause it's late and your mama don't know %>
Sword

A Simple Router on Rack

This past weekend, I found myself playing around with Rack—a “a minimal interface between webservers supporting Ruby and Ruby frameworks.” Rack is—as you may know—one of the integral interfaces implemented in Rails. The config.ru file in the root of every Rails application is commonplace in Rack applications. You consult this file each time you start the Rails server, and you can use it to insert middlewares into your stack.

While the “writing a lightweight Rack framework” exercise foreshadows another accidental, not-so-capable Sinatra, it can be fun and informative to experiment. Plus, when you’re in a rush to get the website up for next summer’s frozen banana stand, your most efficient route is most certainly framework development. And so I began.

Rack in Action

My progress thus far isn’t extraordinary, but I’m happy to have had the pleasure of deploying something non-Rails on Heroku. (Heroku has a nice guide on deploying a Rack application.) Progress aside, though, I’d like to focus on two facets of the router implementation: block-style configuration and mapping/decorating requests.

Adding Routes

Let’s begin with block-style configuration: This is a fairly common pattern. You’ve seen it in various Ruby gems and—yes—in routes.rb. Some might call it sexy. I’ll call it “Ruby and its wonderful DSL-building features.” The bulk of my implementation is below.

class Router
  class << self
    attr_accessor :routes
  end

  def self.routes
    @routes ||= Routes.new
  end

  def self.load
    yield(routes)
  end

  ...

  class Routes < Hash
    def add(verb, path, mapping)
      rkey = "#{verb}##{path}"
      self[rkey] = mapping
    end
  end
end

First, we access the eigenclass of Router and define a reader/writer. This allows us to access the @routes class-level instance variable (initialized just below) directly from class methods. In this case, we define a Routes class that inherits from Ruby’s Hash and implements a single instance method—Routes#add. This method will be responsible for mapping keys (created by the unique combination of an HTTP verb and a URL path) to controller mappings (e.g., users#show). We define self.routes to be a new instance of our Routes class. Lastly, we include a self.load method, wherein we yield self.routes to a block.

Let’s see the Router class at work and define some routes.

Router.load do |routes|
  routes.add "get", "/", "pages#index"
  routes.add "get", "/users", "users#index"
  routes.add "post", "/users", "users#create"
end

As you may have noticed, our DSL for building up routes is a bit different than that implemented in Rails, but it has the same feel. Behind the scenes, we’re simply building up a Routes object with unique keys mapped to values that have meaning within the context of our framework.

Routing and Decorating Requests

So far, we have an augmented hash that maps verb-path keys to controller actions. Great. But how do we actually make use of these mappings? Furthermore, if we want to use something like a dynamic path segment (the :id in /users/:id), how do we go about sending the dynamic value along with the request?

Let’s start by looking at our application class, OnRackApp.

class OnRackApp
  def process_and_respond(env)
    request = Rack::Request.new(env)
    response = Rack::Response.new

    response = Router.new.route(request, response)
    response.finish
  end
end

Rack provides us with some helpful classes for handling requests and responses, Rack::Request and Rack::Response, respectively. The Rack::Request class, for instance, provides multiple params methods for retrieving request parameters (just like you see in the Rails controller). For now, we’ll isolate these class names in OnRackApp and allow our Router class to expect certain interfaces of request and response, rather than objects of specific classes. For some reason, I elected to take a more functional approach here, instantiating a Router object with no arguments and passing the request and response objects to the Router#route method. (Maybe this should be reconsidered?)

Let’s check out Router#route. As we move through these methods, you will continue to see a more functional style of data passing. There is mutation, but it does not occur on the state of the object. Rather, it occurs on the passed-around data. Again, there is design to be done here.

class Router
  ...
  def route(request, response)
    if mapping = map_and_parameterize(request)
      map_and_invoke_respond(mapping, request, response)
    else
      respond_to_absent_mapping(response)
    end
  end
  ...
end

Now our router has access to a request object, which has access to—among other properties—getters for request_method and path. You’ll recall that these are the two components that we used to create the unique route key (where request_method is the HTTP verb and path is the path). Thus, we can combine these properties to create a key with which we can access the controller#action mapping. We can route a request.

class Router
  ...
  private

  def map_and_parameterize(req)
    rkey = "#{req.request_method}##{req.path}"
    if mapping = Router::routes[rkey]
      return mapping
    end
    ...
  end
end

What happens, though, when we define a path with a dynamic route segment (e.g., /users/:id)? When we field a request for this route, we certainly won’t find it in this format. Instead, we expect to see a request path like /users/22. So, for dynamic route segments, our simple key construction falls flat.

Hope remains, of course. Because we receive a request path that has the same pattern as the dynamically segmented path we defined, we can in effect use pattern matching (Regexp) to not only find the appropriate mapping, but also to extract the segment name and value. I’m not by any means satisfied by the rigid approach I’ve taken to date, but it is functioning. That always makes for happy rabbits. You can check out router.rb here.

You’ll see that—because we can extract each dynamic route segment—we can add it to the request params (or further decorate the request object) before passing it along to the controller. Pretty neat stuff.

Racking Up

While there are plenty of gaps in the router and framework architecture we’ve discussed here, there is a real benefit in thinking through this kind of design. Ruby affords us the ability to create fun domain specific languages (DSLs), and building your own half-functional framework can be a great way to experiment with different techniques and language constructs. And you can make a frozen banana stand website, too.