Ruby on Rack #2 - The Builder
Published over 5 years ago

In Ruby on Rack #1 – Hello Rack! we used rackup to make port/server configurable. And rackup’s config file looked like :

# config.ru
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "Hello Rack!"]}

Under the hood, rackup converts your config script to an instance of Rack::Builder.

What is Rack::Builder ?

Rack::Builder implements a small DSL to iteratively construct Rack applications.

- Rack API Docs

Rack::Builder is the thing that glues various Rack middlewares and applications together and convert them into a single entity/rack application. A good analogy is comparing Rack::Builder object with a stack, where at the very bottom is your actual rack application and all middlewares on top of it, and the whole stack itself is a rack application too.

Let’s say our rack application is called infinity :

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
Rack::Handler::Mongrel.run infinity, :Port => 9292

All infinity does is send the env hash inspect string back to the browser.

Now, there are three important Rack::Builder instance methods that you should care about :

1. Rack::Builder#run

Rack::Builder#run specifies the actual rack application you’re wrapping with Rack::Builder.

Converting infinity to use Rack::Builder:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new
builder.run infinity
Rack::Handler::Mongrel.run builder, :Port => 9292

Or you can follow the community convention and use the block form of Rack::Builder :

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new do
  run infinity
end
Rack::Handler::Mongrel.run builder, :Port => 9292

Here Rack::Builder#initialize accepts a block argument, which is evaluated within the context of newly created instance using instance_eval.

2. Rack::Builder#use

Rack::Builder#use adds a middleware to the rack application stack created by Rack::Builder. If the term “middleware” confuses you, don’t worry. Hopefully my next post will clean the air. Stick to the before/after/around filter analogy for now.

Rack has many useful middlewares and one of them is Rack::CommonLogger, which logs a single line to the supplied log file in the Apache common log format.

So if we’re to add Rack::CommonLogger to infinity :

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new do
  use Rack::CommonLogger
  run infinity
end
Rack::Handler::Mongrel.run builder, :Port => 9292

Line of interest is of course use Rack::CommonLogger. As we didn’t supply Rack::CommonLogger with an explicit logger, by default it’ll log to env[“rack.errors”]. Hence you’ll see logging strings in the console for every request.

3. Rack::Builder#map

Rack::Builder#map mounts a stack of rack application/middlewares the specified path or URI and all the children paths under it.

Let’s say you want to show “infinity 0.1” for all the paths under /version ( i.e. /version, /version/whatever BUT NOT /versionsomething ) , you might want to do something like :

require 'rubygems'
require 'rack'

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new do
  use Rack::CommonLogger
  run infinity

  map '/version' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
  end
end
Rack::Handler::Mongrel.run builder, :Port => 9292

But that’s not going to work. Rack::Builder#map also encapsulates a scope within the builder. And one scope can just have one Rack::Builder#run method. In the example above, we have run infinity at the top level global scope and map ‘/version’ has it’s own run too. Hence the conflict.

To fix this:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new do
  use Rack::CommonLogger

  map '/' do
    run infinity
  end

  map '/version' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
  end
end
Rack::Handler::Mongrel.run builder, :Port => 9292

Now if you go to http://localhost:9292/version or http://localhost:9292/version/1 or even http://localhost:9292/version/whatever/doesnt/matter, you’ll see “infinity 0.1” and for all the URIs not starting with /versionhttp://localhost:9292 – you’ll see the env hash inspect string!

Please note that :

  1. /versionsomething WILL NOT show the version, but will display the env inspect.
  2. When you have multiple map blocks, URIs are tried from longest length to shortest length.

Nesting map blocks

Let’s say you feel like adding information about last version. So to show “infinity beta 0.0” at /version/last:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new do
  use Rack::CommonLogger

  map '/' do
    run infinity
  end

  map '/version' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
  end

  map '/version/last' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] }
  end
end
Rack::Handler::Mongrel.run builder, :Port => 9292

Above code will work perfectly as expected. You’ll see “infinity beta 0.0” at http://localhost:9292/version/last and “infinity 0.1” at http://localhost:9292/version.

But a better way (IMHO) to write the same code is by nesting map blocks :

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}
builder = Rack::Builder.new do
  use Rack::CommonLogger

  map '/' do
    run infinity
  end

  map '/version' do
    map '/' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
    end

    map '/last' do
      run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] }
    end
  end
end
Rack::Handler::Mongrel.run builder, :Port => 9292

This works perfect. When you nest map blocks, you’ll need to specify URI relative to the enclosing mapping block, as you can clearly see in the example above.

Rack::Builder → rackup

As I mentioned above, rackup converts the supplied rack config file to an instance of Rack::Builder. This is how is happens under the hood ( just so you get an idea ) :

config_file = File.read(config)
rack_application = eval("Rack::Builder.new { #{config_file} }")

And then rackup supplies rack_application to the respective webserver :

server.run rack_application, options

Very straight forward! In short, rack config files are evaluated within the context of a Rack::Builder object. So if we convert infinity to a rack config file which rackup can understand :

# infinity.ru

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.inspect]}

use Rack::CommonLogger

map '/' do
  run infinity
end

map '/version' do
  map '/' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity 0.1"] }
  end

  map '/last' do
    run Proc.new {|env| [200, {"Content-Type" => "text/html"}, "infinity beta 0.0"] }
  end
end

And now run it :

$ rackup infinity.ru