Code Odyssey : Sinatra
In 2012, I am planning to start contribute and participate more on opensource projects.
The target of this series is to read through the source of open source projects that I am interested with,
and explain the structure and interesting pieces that I found in the source.
Sinatra
Sinatra is a rack-base , lightweight web framework implemented in ruby.
Written and desinged by Blake Mizerany. Famous for it’s dsl syntax and simpliness.
Source structure
examples/
lib/
sinatra/
base.rb #all codes are in here
main.rb #Application class, extends Base class in base.rb
showException.rb #output exception and trace message as Html error page
sinatra.rb
test/
Rakefile
Gemfile.gem
base.rb
Main Sinatra application, includes:
Rack Module :
Implement Rack:Request and Rack:ResponseHelper Module :
Helper methods that available in routes, filters and views ,
handle tasks like redirect, status code, url, html header, session, mime type, http stearming… etcTemplate Module :
Handle multiple template engines using tiltBase class:
The main class that include all modules above. Handling routes and invoke correspond code blocks and filters.Application class:
Inherit Base class, the run instance of Sinatra application.Delegator module:
Delegate DSL methods in Top-level file to Sinatra Application.
main.rb
Patch Sinatra::Application class, set the hooks to run application at exit and Parse option. Also it includes the delegator to send all methods on Top-level to application.
Dependencies
Sinatra source seperate the declaration of external ,stdlib and project depedencies.
Which is pretty clean and easy to understand:
1 |
|
Delegator
Delegator is an interesting part in Sinatra, since it creates a really simple API that user can just write method with HTTP verb in Top level file, without creating any class.
For example:
1 | #myapp.rb |
Execute this file will run Sinatra application handle route “/“ with GET request.
But how do Sinatra do this?
Take a look at the source of Delegator:
1 |
|
First, if we want to delegate method to another class, we can include the methods in files :
1 |
|
But how would we do if we have lots of method to delegate? In Sinatra, it has lots of methods and Http verbs to be delegated. The code will be pretty ugly if we have to implement all these repeated methods.
The answer here is metaprogramming: We can use ruby’s ability of metaprogramming to create repeated methods in a few lines of code:
1 |
|
In ruby, we can use “define_method” to create method programmically, and use “send(method_name, *args, &block)” To call the target method by the method_name. This makes the code a lot cleaner in Sinatra
Routes
In sinatra, after user call the dsl methods(like get, post) in file,
the HTTP verbs ,path and code block will be registered in application,
And will be executed when receiving matched request.
When the dsl method get called, the Application will generate a Proc with the name of
HttpVerb and path (like “get /“) save the Proc, url path (include the keys, pattern and conditions on paths, like “/:id” ) in @routes.
Here’s the simplify version of routes :
1 |
|
In here, sinatra generates the code block as an unbound_method, it is a kind of instance method that you can bind it to any other instance dynamically before call. Sinatra use this to bind Application instance with Proc on runtime.
##Route call
After register the code block, sinatra wait for request and invoke correspond routes to handle request.
The entry point of all request is the rack call interface. All rack application must implement the interface.
Overall, the request execution stack is:
call => call! => invoke => dispatch! => route! => route_eval
1 |
|
The code above is the first part of how Sinatra handle incoming requests.
First, as a Rack application, all request will invoke the call(env) function
Sinatra application will duplicate an instance, invoke the call!(env) on new instance (because HTTP is stateless)
in the call! function, sinatra will new the Rack::Request and Rack::Response object by env, than set the params.
After all object is set, it will start to invoke the routes by “invoke{ dispatch! }”, the result will be store
on @response, and return to user by call the @response.finish
1 | # Run the block with 'throw :halt' support and apply result to the response. |
The invoke function wrap and execute the handler codeblock ,catch the :halt
(which throw by route! as interrupt signal), and than set status, header and result to @response.
for example, when you execute the code wrapped by invoke, you can set the @response by throw :halt and Array response:
invoke do
#do something...
throw :halt ,[200,"Hello world!"] #this will go to @response
end
With the structure like this, error_block or other function can also throw :halt with result and return to user.
1 |
|
In dispatch function, it check the static file first, than execute before filter, then execute the route! function follow by the after filter.
1 |
|
At the bottom of execution stack, the route! function check the registered routes with request path and params.
If it find correct route, execute the codeblock and throw :halt with result. the invoke function will catch the :halt,
than set the result to @response.
If no route is executed, route_missing will be called and return not_found page.
Template
Sinatra is compatible with a lots of different templates, from erb, haml, markdown to sass, less…
almost any kind of templates that you can find, but how do Sinatra handle all of these different format?
It turns out using Tilt gem that includes all kinds of template engines.
For example, with Tilt, we can compile an erb template like this:
1 |
|
In application, we can call “erb” method to render erb template:
get "/" do
#render erb template in views/index.html.erb
erb :index
end
under the hood in Template module:
1 |
|
First, the helper method will call the render method with format,
and render method compile the template and return output, than output will be catch by invoke method (in previous section)
and set to @response.
In compile_template, the Tilt engine will be called and return correct Tilt::Template instance.
here’s the digest version:
1 |
|
The template_cache is an instance of Tilt::Cache, is a very simple hash implementation of cache:
class Cache
def initialize
@cache = {}
end
def fetch(*key)
@cache[key] ||= yield
end
def clear
@cache = {}
end
end
end
Streaming
Stream is another interesting part in Sinatra, and probally one of the most complex part.
It use the EventMachine to implement streaming APIs that let you able to keep
sending data asynchronize without I/O blocking.
For example,
get '/' do
stream :keep_open do |out|
out << "hello "
EventMachine.defer do
#something slow...
sleep(3)
out << "world"
end
end
end
will output the responses chunk to user while the content is ready, and keep the connection open.
For doing that, it use the EventMachine.defer , EventMachine.schedule to create threads to avoid i/o blocking while generating result.
1 |
|
What stream method do is, first it detect the Server is support streamming or not. If so, use EventMachine.
And it wrap the code block with params, create a Stream instance than send it to body helper.
and body helper will send stream to Rack::Response.
1 |
|
According to Rack interface, the response body need to respond to “each” method.
The each method will be called with &front block, which can sent result to user.
Stream class use the EventMachine.schedule to call codeblock asynchronizly,
and the << method will sent data to @front with EventMachine.schedule.
Configure
In sinatra, we can set configuration by “set” or “configure” method.
set :server , :thin
#or
configure do
set :server, :thin
end
what configure do here is just call yield self, and act as a place for all settings.
And also those 2 methods are delegated methods.
What set doing here is a little different with normal setting methods:
It use the metaprogramming skills again.
while we call the set method,
it will generate getter and setter methods for self.server
configure do |app|
set :server, :thin
app.server # => :thin
app.server = :unicorn # Application.server => :unicorn
end
1 |
|
Conclusion
Sinatra is a very simple and delegate web framework. It takes lots of advantage on ruby’s metaprogramming feature
to make code more digest and clean. Also with decent features support.(Template, Streaming, Filter, Route…)
The dsl syntax and delegator makes learning Sinatra application become very easy.
It will be great for implement api service or small website when you don’t need the heavy stacks like Rails.
Comments