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_URIprovides the full request endpoint including protocol and endpoint and query string parameters.PATH_INFOorREQUEST_PATHprovides just the resource.QUERY_STRINGbreaks out the query string parameters delimited by the&character.REQUEST_METHODindicates the HTTP verb used for the request. Thecurlcommand issuesGETrequests 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 NoMethodErrors 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.