Ruby on Rack #2 - The Builder 13

Posted by pratik
on Tuesday, November 18

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

1
2
# 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 :

1
2
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:

1
2
3
4
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 :

1
2
3
4
5
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 :

1
2
3
4
5
6
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 ) :

1
2
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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
Comments

Leave a response

  1. kaineerNovember 18, 2008 @ 05:07 AM

    there’s a typo there: > $ rackup inifnity.ru $ rackup infinity.ru

  2. PratikNovember 18, 2008 @ 05:09 AM

    @kaineer : Fixed, thanks!

  3. AdamNovember 18, 2008 @ 10:05 AM

    So the “use” keyword (“use Rack::CommonLogger”) is a form of dependency injection?

  4. Jeremy GailorNovember 18, 2008 @ 05:22 PM

    Awesome articles. This is just what I’ve been looking for on rack to start digging in and poking around. Thanks for putting them up.

  5. PratikNovember 18, 2008 @ 05:51 PM

    @Adam Kinda. I hope my next article will help.

    @Jeremy Glad to help!

  6. postmodernNovember 18, 2008 @ 08:06 PM

    I was just looking for an article exactly like this one. Thanks for posting this, really helpful.

  7. andyNovember 19, 2008 @ 03:41 PM

    @Pratik – thanks for this writeup.

    In a Sinatra app, how would you configure Rack::CommonLogger to send output to a log file rather than stdout ??

  8. forrestNovember 20, 2008 @ 06:27 AM

    Rack is awesome. I actually just started playing with it a few days ago and then stumbled on your two (very recent) articles. Nice timing!

    I’m thinking I’ll use Rack along with Sequel for a project (as well as some other new stuff that I haven’t used yet) I’m working on.

    Some other cool Rack-ish stuff to look at if you haven’t already: Rack::Cache, mod_rack/mod_rails/Phusion Passenger.

  9. postmodernNovember 20, 2008 @ 11:18 PM

    I just started getting this error when running your #map example.

    Thu Nov 20 15:36:15 -0800 2008: Read error: #<nomethoderror:><array:0x7f899f4b8ab8>> /usr/lib64/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/commonlogger.rb:20:in `_call’ /usr/lib64/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/commonlogger.rb:13:in `call’ /usr/lib64/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/builder.rb:53:in `call’

  10. Matt ToddNovember 21, 2008 @ 07:07 AM

    Damn good articles, Pratik. Explaining the value of Rack has always been difficult to me, even as someone who has developed a framework built on Rack. Middleware is one of the hardest, simplest concepts to really drive home.

    Some thoughts:

    I never write Rack::Builder manually, I almost always put my scripts in rackup config files or as class definitions as proper middleware.

    Also, make sure you check out Merb’s middleware definitions, will probably find something useful for your next article.

    Thin as an excellent implementation for creating a CLI interface in Thin::Runner. I used something very similar for Halcyon’s CLI util. It’s much cleaner (though a bit more complex) than looking at rackup for inspiration.

    Keep up the good work, looking forward to the next article!

  11. samNovember 25, 2008 @ 03:49 AM

    Great post!

    Rack is really a great tool. It is being used in many ruby web frameworks. I know that Ruby Waves was built on it.

    Thanks

  12. Graham AshtonDecember 18, 2008 @ 11:13 AM

    @andy – I’ve just been trying to work out how to get Sinatra to use CommonLogger too, and I’ve come up with this (seems to work, but I’ve not really hit it much yet):

    require 'logger'
    use Rack::CommonLogger, Logger.new("foo.log")
  13. Bala ParanjDecember 20, 2008 @ 02:44 AM

    Since Rack provides access to Session how can I implement Single Signon for two Rails apps running on Passenger?

Comment