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

Ruby on Rack #1 - Hello Rack! 6

Posted by pratik
on Monday, November 17

Ruby community is coming up with new frameworks almost every week, but in midst of that, Rack isn’t getting enough attention. Attention that it deserves. And also, the next stable release of Rails after 2.2 will have a better public facing interface for taking full advantage of Rack.

Rack was initially inspired from pythons’s wsgi and it quickly became the de-facto web application/server interface in the ruby community, thanks to it’s simplicity and preciseness. You might want to read Introducing Rack from the creator of rack – Christian Neukirchen before reading this post.

What is Rack ?

Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.

- Rack API Docs

Practically speaking, you can divide “Rack” in two parts :

Rack Specification

Rack specification specifies how exactly a Rack application and the web server should communicate :

A Rack application is an Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body.

- Rack Specification

That’s the specification in a nutshell. You can check out the full details here.

Strictly speaking, you don’t need the rack gem in order to write Rack ready applications. Just stick to the specification and that’s it.

Rack Gem

Rack gem is a collection of utilities and facilitating classes, to make life easier for anyone developing Rack applications. It includes basic implementations of request, response, cookies & sessions. And a good number of usefult middlewares. In short, install the rack gem. You’re gonna need it :

$ sudo gem install rack

To summarize

  • Rack is a framework to roll your own ruby framework.
  • Rack provides an interface between different web servers and your framework/application. Making it very simple for your framework/application to be compatible with any webserver that supports Rack – Phusion Passenger, Litespeed, Mongrel, Thin, Ebb, Webrick to name a few.
  • Rack cuts your chase. You get request, response, cookies, params & sessions for free.
  • Makes it possible to use multiple frameworks for the same application, provided there is no class collision. Rails and sinatra integration is a good example of this.
  • Middlewares ! Think of middlewares as Rails’s before_filter/after_filter that are reusable across different rack supported frameworks/applications. For example, you can use the same Anti-spamming rack middleware for your Rails app, Sinatra app and your custom Rack application too!

Examples

Let’s start with a smallest possible example of a rack application, using mongrel.

1
2
3
4
5
6
7
8
9
10
require 'rubygems'
require 'rack'

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/html"}, "Hello Rack!"]
  end
end

Rack::Handler::Mongrel.run HelloWorld.new, :Port => 9292

The above code passes an object of HelloWorld to the mongrel rack handler, and starts the server on port 9292.

The HelloWorld object here respects the rack specifications. That is :
  1. Responds to call(), which takes one argument – environment
  2. call() returns an Array of [http_status_code, response_headers_hash, body]

That’s all ! If you run this script and browse to http://localhost:9292, you’ll see the shiny “Hello Rack!” message.

But hey, even a ruby proc responds to call(). So why not use a proc instead ? Well, no reason not to :

1
2
3
4
require 'rubygems'
require 'rack'

Rack::Handler::Mongrel.run proc {|env| [200, {"Content-Type" => "text/html"}, "Hello Rack!"]}, :Port => 9292

Another common seen pattern is to use method(:something), which returns an object of Method class :

1
2
3
4
5
6
7
8
require 'rubygems'
require 'rack'

def application(env)
  [200, {"Content-Type" => "text/html"}, "Hello Rack!"]
end

Rack::Handler::Mongrel.run method(:application), :Port => 9292

Take that you “Hello World” performance retards. You’re not gonna be able to write a faster ‘Hello World’ ruby application than this.

Rack it up’

As I said earlier, rack gem comes with a bunch of useful stuff to make life easier of a rack application developer. rackup is one of them. In the previous examples, I had used the mongrel handler Rack::Handler::Mongrel directly, and even hard coded the port number. With rackup, these things become configurable ! But to use rackup, you’ll need to supply it with a rackup config file. For our above example, the config file will look somewhat like :

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

Just a line. By convention, you should use .ru extension for a rackup config file. Supply it a run RackObject and you’re ready to go :

$ rackup config.ru

By default, rackup will start a server on port 9292. But you can override that with a -p option to rackup. For more help, RTFM:

$ rackup --help