If you're writing web applications with Ruby there comes a time when you might need something a lot simpler, or even faster, than Ruby on Rails or the Sinatra micro-framework. Enter Rack.
Rack describes itself as follows:
Rack provides a minimal interface between webservers supporting Ruby and Ruby frameworks.
Before Rack came along Ruby web frameworks all implemented their own interfaces, which made it incredibly difficult to write web servers for them, or to share code between two different frameworks. Now almost all Ruby web frameworks implement Rack, including Rails and Sinatra, meaning that these applications can now behave in a similar fashion to one another.
At it's core Rack provides a great set of tools to allow you to build the most simple web application or interface you can. Rack applications can be written in a single line of code. But we're getting ahead of ourselves a bit.
Installing Rack is simple. You just need to install the rack
gem.
gem install rack
I would also recommend installing a Rack compatible server, such as thin
, but Webrick
should work just as well.
gem install thin
With Rack installed let's create our first Rack application. Like I said Rack applications can be written in just one line of code, should we desire.
run ->(env) { [200, {"Content-Type" => "text/html"}, ["Hello World!"]] }
We can run this application like such:
rackup lambda_example.ru
If we now visit http://localhost:9292
we should be greeted with Hello World!
as we expect.
So how did this work? Rack has two simple requirements. The first is that whatever you ask it to run must respond to a call
method, which is why a lambda
or Proc
works here. This call
method will be called with a Hash
-like object that contains the request, as well as other environmental data.
The call
method must return an Array
that contains three elements.
The first element is an integer that represents the status of the request, so in our example we return a status of 200
.
The second element is a Hash
that contains any headers you want to return with the response.
The last element is the body of the response. Whatever object you return here must be an enumerable object, such as an Array
or an IO
object.
Since Rack will accept any object that responds to a call
method we can enhance our example further by using a full Ruby class instead of just a lambda
.
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/html"}, ["Hello World!"]]
end
end
run HelloWorld.new
We're now doing the same thing, but we are using a class instead of a lambda
.
As I said earlier Rack provides a great toolkit for writing Ruby applications. So let's use a little bit of that in this application. Let's add a few routes.
class HelloWorld
def call(env)
req = Rack::Request.new(env)
case req.path_info
when /hello/
[200, {"Content-Type" => "text/html"}, ["Hello World!"]]
when /goodbye/
[500, {"Content-Type" => "text/html"}, ["Goodbye Cruel World!"]]
else
[404, {"Content-Type" => "text/html"}, ["I'm Lost!"]]
end
end
end
run HelloWorld.new
First we create a new Rack::Request
object using the env
hash that was passed in from the request. We can then use the path_info
attribute on the request and build a simple case
statement to handling our "routing".
If the path matches /hello/
we'll return a status of 200
and the body of "Hello World!"
. If the path matches /goodbye/
we'll return a status of 500
and the body of "Goodbye Cruel World!"
. All other requests will get a 404
response.
Now if we open a browser and navigate to http://localhost:9292
we should see the 404
page because none of our routes matched. We can confirm that status of 404
in the browsers inspector window.
Navigating to http://localhost:9292/hello
and http://localhost:9292/goodbye
give us the results we would expect.
Fantastic, we now have some simple routing in our application, however, this won't scale very well, and case
statements aren't going to get us where we need to be.
Let's take this example one step further and build a simple web framework that will handle GET
requests. I'll leave it up to you to support the other HTTP protocols.
Let's start with what we want the application to look like, and then we'll fill in the details when we write our simple framework.
$:.unshift File.dirname(__FILE__)
require 'simple_framework0'
route("/hello") do
"Hello #{params['name'] || "World"}!"
end
route("/goodbye") do
status 500
"Goodbye Cruel World!"
end
run SimpleFramework.app
The first few lines are simply there to add the current directory to Ruby's load path and require, what will be, our framework.
Our framework, named SimpleFramework
, gives us a route
method that takes a path and a block. If the request matches the path then the block will be evaluated and the last line of the block will be the body of the response.
In the hello
block you can see we are referencing a params
hash, so we'll need to make sure that is available to the block. In the goodbye
block we want to set the status of the response to 500
, so we'll need a status
method that let's us do that.
Now that we know what we want the SimpleFramework
to look like, let's actually implement it.
require 'action'
class SimpleFramework
def self.app
@app ||= begin
Rack::Builder.new do
map "/" do
run ->(env) {[404, {'Content-Type' => 'text/plain'}, ['Page Not Found!']] }
end
end
end
end
end
def route(pattern, &block)
SimpleFramework.app.map(pattern) do
run Action.new(&block)
end
end
The first thing we are doing is requiring a filed named action
, we'll look at that in just one second. Let's look at the SimpleFramework
class first.
The SimpleFramework
class is quite simple, thanks to the the Rack::Builder
class that Rack offers us. The app
method we've defined will return an instance of the Rack::Builder
class. Rack::Builder
let's us easily construct a Rack application by letting us chain together a bunch of smaller Rack applications.
In the app
method when we create the instance of the Rack::Builder
class we are going to map a default Rack application to run, should no other path match.
Finally we define a route
method at the root object space to let us map our actions.
The real meat of SimpleFramework
is the Action
class, and even that is pretty simple and straight forward.
class Action
attr_reader :headers, :body, :request
def initialize(&block)
@block = block
@status = 200
@headers = {"Content-Type" => "text/html"}
@body = ""
end
def status(value = nil)
value ? @status = value : @status
end
def params
request.params
end
def call(env)
@request = Rack::Request.new(env)
@body = self.instance_eval(&@block)
[status, headers, [body]]
end
end
When we initialize a new instance of the Action
class we define a few attr_reader
methods and set them to some basic defaults.
We implement the status
and params
methods so we have access to them as we saw earlier.
Finally, we implement a call
method that takes in the env
hash and creates a Rack::Request
object from it. We then use instance_eval
to evaluate the original block in the context of the Action
object so it has access to all of the methods and goodies in the Action
class.
All that is left to do is return the appropriate Rack response Array
.
When we navigate to http://localhost:9292/hello
in the browser we see Hello World!
just as expected. If we pass name=Mark
on the query string we should now see Hello Mark!
proving that we do have access to the request parameters, just like we wanted.
A quick look at http://localhost:9292/goodbye
confirms that we are getting a status of 500
and the text of Goodbye Cruel World!
.
Well, that's a quick look at the basics of the Rack library. There is a lot more to it, and I highly suggest you read its very thorough documentation.
One word of caution though, from someone who's been there, it is very easy to go down this path and end up building your own fully featured framework. Before you do that, make sure that something like Sinatra, doesn't already float your boat.
That's it, for now. I hope this helps.
Thank You!
This article describes the basics really very well!