Ruby on Rack #2 - The Builder
Published over 4 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.
Rack::Builder implements a small DSL to iteratively construct Rack applications.
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 :
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.
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.
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 /version – http://localhost:9292 – you’ll see the env hash inspect string!
Please note that :
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.
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