In this post, I write a small domain specific language to handle HTTP requests (inspired by Sinatra) using Rack. We start by taking a quick look at a few small rack applications and then write a small DSL.
Rack
Rack provides an abstraction layer between multiple web servers (unicorn, webrick, thin, etc) and your ruby application. This allows developers to focus on the application layer instead of working on handling the low level details associated with dealing with a HTTP request.
Let’s take a look at the “hello world” example provided by Rack:
1 2 3 4 5 6 7 |
|
To use rack, one must provide an object (class, proc or lambda) that responds to call
and returns an array with
elements. The first element is a stringified HTTP status code. The second is a hash of response headers. The last
element is the response body, which must respond to the each
method.
Let’s run the rack app in one terminal and issue a HTTP request against it in a different one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Let’s supply a class that implements a call
class method. Let’s also use awesome_print
to interrogate the env
parameter:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Let’s issue a HTTP request against this app using the bash command curl -v localhost:8080/foobar?baz=true
and observe
the app output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
The env
hash provides some insight into the the facilities that Rack provides. Let’s take a look at some of the HTTP
request components:
REQUEST_URI
provides the full request endpoint including protocol and endpoint and query string parameters.PATH_INFO
orREQUEST_PATH
provides just the resource.QUERY_STRING
breaks out the query string parameters delimited by the&
character.REQUEST_METHOD
indicates the HTTP verb used for the request. Thecurl
command issuesGET
requests by default unless we specify one via the-X <HTTP_VERB>
parameter.- Variables that start with
HTTP_*
correspond to HTTP request headers (e.g.HTTP_ACCEPT
=Accept
).
The Rack interface specification provides more details.
Rack additionally provides middleware facilities like routing to different apps Rack::URLMap
, Apache-like logging
Rack::CommonLogger
, exception handling Rack::ShowException
and Rack::File
for serving static assets. There are
numerous rack middleware and utilities available as well via rack-contrib.
A Simple DSL
A domain specific language is designed to solve problems for a particular application. In our case we want to define a simple language to handle HTTP requests. While predominantly a Rails developer, I am enchanted by Sinatra’s minimal and expressive syntax. After digging into Sinatra’s source I felt inspired to write a small DSL for fun.
My goal is register a block of code that gets executed when a HTTP request is made to a particular endpoint:
1 2 3 |
|
We want to associate '/foobar'
with the block of code, which is defined within the do ... end
block. A hash seems
like a convenient data structure for this purpose. First, let’s define a class called RackTest
that responds to
call
with a 404
not found (we will change this later). Also, let’s define an add_callback
method that takes the
route as a string and associates it with a block of code in a @callback
hash.
1 2 3 4 5 6 7 8 9 10 |
|
Now we need to define a get method outside of the RackTest
class:
1 2 3 |
|
Finally, let’s put everything together. We need to call the block of code associated with a REQUEST_PATH
from the
incoming HTTP request. If no endpoint is registered in the @callback
hash, then we want to return a 404
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Let’s exercise this code via curl:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Our DSL is pretty limited, let’s look into extending it. Minimally, I want to be able to specify the HTTP status code and content type in the HTTP response headers.
Let’s start with how we want to use the DSL in the MyRack
class:
1 2 3 4 5 6 7 8 9 10 11 |
|
It is important to understand that the do ... end
block is executed in the context of the call(env)
method.
Therefore we would see a NoMethodError
s for the status
and content_type
method calls because these methods are not
defined in MyRack
or RackApp
. We want to direct these method calls to a new class.
This class will serve as a data structure for our HTTP response data. The status
and content_type
methods will
return stored instance variables or set them depending on whether the parameter is specified. This is purely syntactic
sugar for the DSL.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Now let’s define the RackApp
parent class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
We return early with a 404
unless @registry[env['REQUEST_PATH']]
was defined. Otherwise, we call the block
stored in our HttpResponse
instance. Executing this block may result in status
and content_type
calls, but in the
context of RackApp
. The method_missing
method proxies the message to our response instance. The name
is the
symbolized name of the method and the args
are forwarded to this method.
For simplicity, this minimal example does not differentiate between HTTP verbs. We could easily extend the @registry
hash to do so by adding nesting for each verb:
1
|
|
We could add support for processing query string params via Rack::Request
class. There are numerous possible
improvements for our DSL. However, the goal here was not to recreate a robust DSL like Sinatra, but to show how easy
it is to use Rack to get started along that path.