When we started working on Deltacloud API rewrite to Sinatra, we realized that creating routes for every single Deltacloud collection can be pretty frustrating. Usually, when you writing REST based application in Sinatra you end-up with typing the same CRUD operations for every collection of resources your application have.
This was a bit frustrating, since many operations are similar and basically doing the same thing. For example the 'show' operation is usually defined like this:
Basically you type this code over and over for all
The only thing you change is the 'item'.
Also if you want to have your API easy discoverable for clients, you also need
to define OPTIONS and HEAD routes to advertise supported operations or
The powerful weapon that Ruby allows to programmers do, is easy DSL creation. So we decided to move over and replace the non-DRY, hard to read code with some elegant DSL. We call it Rabbit and we basically use it to serve all collections and operations. The only mistake we made in time when we started to write Rabbit was that we use classic non-modular Sinatra style.
Since Rails and other frameworks support mounting of 'rack' applications, the importance of having the 'modular' Sinatra applications is growing up. And we need to somehow follow this movement too.
As a first step I started working on moving Rabbit (our DSL) away from the Deltacloud project to make it as upstream rubygem, that could be included into any Sinatra application (both modular and non-modular). I created a repository on Github and today, version 1.0 was released.
Introduction to Rabbit
Let start with very simple, REST based, modular Sinatra app:
In begging, we need to require the
extension. This extension is shipped as rubygem and can be installed using
gem install sinatra-rabit. Requiring Rabbit isn't invasive and
will not extend the Sinatra::Base class with Rabbit methods automatically. To do
so, you would need to do
include Sinatra::Rabbit inside your
Sinatra::Base class (MyApp in this case). This allows you to use the DSL
syntax sugar in MyApp class, but also keep the good old Sinatra routes and helpers
collection method is used to declare a new resources collection.
The one in example above represents 'images'. All collections and operations can have
description set using the
description method. This description will be
used later for automatic documentation generation. This is not implemented yet.
The DSL define standard set of CRUD operations and will automatically add
corresponding HTTP method to each operation. For example the
operation will automatically be defined as
POST /images and the
:destroy operation will get the HTTP DELETE method.
Operations can also use various POST/GET parameters. For example in the
:create operation the
:name parameter is required.
But as you see, there is no validation in the
control block. Trick
is that the parameter validation is done automatically. So whenever the 'create'
operation is called without the 'name' parameter, Rabbit automatically reply with
the 400 HTTP status code, including missing parameter in the HTTP body response
for the client.
control block is used to determine what code will be executed
when client access the operation. You can use all Sinatra helpers, like template
helpers (haml, sass, ...) or flow helpers (halt, status, ...) freely inside this
With the power of DSL, you can play with the code more and make it more abstract and mote easy for user to understand. Besides route generation and parameter validation, Rabbit also support more advanced features which can be handy:
:if option in the operation definition will make 'skip'
that operation if the condition given as value is evaluated as false.
In some specific use-cases, you want to have sub-collection of resources. In
example above, we have collection
:buckets. This collection
represents something like a directory, which can store files (blobs). You can
have multiple buckets and each bucket can have multiple blobs.
Rabbit will automatically define couple routes around collections or operations. These routes can help to your clients to discover API structure, like to get list of operations defined for collection or list of parameters defined for the operation.
For example requesting
OPTIONS /images (OPTIONS is HTTP method)
will give you list of available operations in 'images' collection. Also
OPTIONS /images/create will give you list of
parameters that this operation supports.