At this point you should be feeling fairly comfortable writing Sinatra applications. So far, we’ve focused primarily on the classic approach, where a single application exists in a single process. That’s only scratching the surface of Sinatra’s potential, however. Let’s take a deeper look at how Sinatra actually works. Once you understand what is going on backstage, it becomes significantly easier to take full advantage of the available API (or even extend it). To gain this understanding, we’ll dig into the source code and take a guided tour of what’s actually going on.
Sinatra follows Semantic Versioning, which basically states that Sinatra will not break backwards compatibility unless the major version (the first number of the version) is increased. So, any application written for Sinatra 1.2.3 will still work with Sinatra 1.3.0. Semantic Versioning requires an official and complete API specification. For Sinatra, this happens to be the README, which you can find here: http://www.sinatrarb.com/intro. You can learn more about Semantic Versioning at http://semver.org/.
Let’s start with a quick experiment. We saw earlier in the book how
parameters are passed into routes and extracted in the context of the
route via the params
hash; for example,
in a simple login form we may find params[:username]
and params[:password]
.
We mentioned previously that the various route definitions in a
Sinatra app (get '/home'
, post '/login'
, etc.) are not themselves method
definitions, but rather calls to methods deeper in Sinatra. The behavior
your route should take is passed as a block (or
closure) for Sinatra to handle and execute.
This gets interesting when examining the context in which the block is executed. A block in Ruby is supposed to have the same methods and variables that the defining scope has. So, what would you expect to see as output from the code in Example 3-1?
$ irb ruby-1.9.2-p0 > require 'sinatra' => true ruby-1.9.2-p0 > get('/') { defined? params } => [/^/$/, [], [], #>Proc:0x00000100aff310@/sinatra-1.2.6/lib/sinatra/base.rb:1152>] ruby-1.9.2-p0 > defined? params => nil
As it turns out, params
is
available only inside the scope of the get
block. The secret to Sinatra’s magic is how
it handles management of self
.
In Ruby, method calls that aren’t sent to a variable or constant
are actually sent to self
; typically
self
is omitted for brevity. Take a
look at Example 3-2, where we call jobs
and self.jobs
; the output is the same because both
calls are made in the same scope and the identity of self
is identical.
$ irb ruby-1.9.2-p0 > jobs => #0->irb on main (#<Thread:0x00000100887678>: running) ruby-1.9.2-p0 > self.jobs => #0->irb on main (#<Thread:0x00000100887678>: running)
The identity of self
becomes
important when crossing what are known as scope
gates; scope gates are sections of code where the context of
the executing code changes, and generally self
changes accordingly. In Ruby, class
definitions, module definitions, and methods are all scope gates.
Example 3-3 shows the results when self
is inspected inside and outside of a
Sinatra route; see Figure 3-1 for the rendered
output.
require "sinatra" outer_self = self get '/' do content_type :txt "outer self: #{outer_self}, inner self: #{self}" end
Technically speaking, closures in Ruby open a new, nested scope. We’ll see more on this in a moment.
What Example 3-3 tells us is that all method calls
in the routing blocks are actually sent to an instance of the class
Sinatra::Application
. Furthermore, if
you refresh the page, take note of the object id; it changes on each
refresh, indicating that there is a separate instance for every request
made.
It’s also worth noting that the outer scope is main
, the same as it was when calling
jobs
and self.jobs
in the IRB session.
So we know now that our routes are passing blocks to instances of
Sinatra::Application
for evaluation.
Where exactly do these route methods live? Let’s open IRB and work with
the Ruby reflection API to get a general idea of the class structure as
demonstrated in Example 3-4.
[~]$ irb ruby-1.9.2-p180 > require 'sinatra' => true ruby-1.9.2-p180 > Sinatra::Application.superclass => Sinatra::Base ruby-1.9.2-p180 > Sinatra::Base.superclass => Object ruby-1.9.2-p180 > method(:get) => #<Method: Object(Sinatra::Delegator)#get> ruby-1.9.2-p180 > Sinatra::Delegator.methods(false) => [:delegate, :target, :target=] ruby-1.9.2-p180 > Sinatra::Delegator.target => Sinatra::Application ruby-1.9.2-p180 > Sinatra::Application.method(:get) => #<Method: Sinatra::Application(Sinatra::Base).get> ruby-1.9.2-p180 > _.source_location => ["~/gems/sinatra-1.3.0/lib/sinatra/base.rb", 1069]
Now that’s interesting. Methods like get
, post
,
and so on are actually defined twice. One
definition is made in Sinatra::Delegator
, which is a mixin extending
Object
. Those methods are therefore
available everywhere in the application. The delegator mixin will simply
send the same method call to Sinatra::Application
, which inherits them from
Sinatra::Base
.
Mixins are a technique Ruby inherited from Common Lisp, which allows you to not only choose a superclass but also to add other class-like objects to the inheritance chain. This enables you to extend already existing classes and objects without overriding other methods accidentally.
Let’s play with Sinatra::Base
and Sinatra::Application
a bit more
to get a better sense of where features are made available. Example 3-5 shows how to properly define route handlers without
using the DSL, and what happens if you try to define them on the wrong
module.
[~]$ irb ruby-1.9.2-p180 > require 'sinatra/base' => true ruby-1.9.2-p180 > get('/') { 'hi' } NoMethodError: undefined method `get' for main:Object from (irb):3 ruby-1.9.2-p180 > Sinatra::Application.get('/') { 'hi' } => [] ruby-1.9.2-p180 > Sinatra::Application.run! == Sinatra/1.3.0 has taken the stage on 4567 for development with backup from Thin >> Thin web server (v1.2.11 codename Bat-Shit Crazy) >> Maximum connections set to 1024 >> Listening on 0.0.0.0:4567, CTRL+C to stop
Although we’ve used the top-level DSL thus far in the book, Sinatra is still perfectly usable without it, requiring only sinatra/base and a little knowledge of the mixin structure to spin up an application. This means that the top-level DSL is totally optional.
Examples 3-4
and 3-5 show that
the source for base.rb contained
the definition for the get
method
(and indeed is the heart of Sinatra as a whole). Close to the end of the
file, you’ll find the implementations of both the Application
class and the Delegator
mixin. The implementation is
surprisingly simple (don’t be too distracted by the register
method; it’s simply to ensure that
methods added by extensions, like
sinatra-namespace, are available from the top-level
DSL as well). All the heavy lifting in Sinatra’s execution takes place
in the Base
class.
The current development version of base.rb can be found at https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb. Replace master with any version (i.e. 1.3.0) to take a look at the file that shipped with it. If you see the content displayed in Figure 3-2, you’ve come to the right place.
Now we’ve armed ourselves with enough knowledge to be a little dangerous with Sinatra. We know now that we don’t have to rely on the DSL, but can engage Sinatra in a completely modular fashion. This begs the question of what we stand to gain by doing so. After all, the DSL is capable and convenient out of the box. What if, however, we want to do something that Sinatra’s DSL doesn’t natively allow?
There are two primary ways to extend Sinatra’s functionality:
extension methods and helpers.
You’ve used extension methods already; an example would be the route
handlers (such as get
) that make up the
average classic application. Usually used at application load time,
extension methods are mostly used for configuration and routing, and map
directly to class methods for Sinatra::Base
or subclasses. It’s the
responsibility of Sinatra::Base
to make
sure everything works properly.
When creating extension and helper methods, it’s considered a best
practice to wrap those in a module and use the register
method to let Sinatra figure out
where to use those modules as mixins. Be kind to your fellow Sinatra
developers!
Let’s take a moment to create a simple extension to Sinatra. Our fictional application for this example requires us to send both GET and POST requests to a particular URL such that the same block of code handles both verbs. We’re Ruby developers, so we try to keep our code DRY and we obviously don’t want to define two routes with identical code. Therefore, it makes sense to define an extension that can handle our requirement without duplication. A simple extension is shown in Example 3-6.
require 'sinatra/base' module Sinatra module PostGet def post_get(route, &block) get(route, &block) post(route, &block) end end # now we just need to register it via Sinatra::Base register PostGet end
Go ahead and create a quick Sinatra app and a module extension in a “sinatra” subfolder; your file should be called post_get.rb. Example 3-7 shows how to actually make use of your new extension.
Once you’ve tried the extension and observed it functioning, try
removing the register PostGet
call
from the module. What happens?
require 'sinatra' require './sinatra/post_get' post_get '/' do "Hi #{params[:name]}" end
Now we can crack open Telnet again and try our multiple route handler.
$ telnet 0.0.0.0 4567 Trying 0.0.0.0... Connected to 0.0.0.0. Escape character is '^]'. GET / HTTP/1.1 Host: 0.0.0.0 HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 Content-Length: 3 Connection: keep-alive Server: thin 1.2.11 codename Bat-Shit Crazy Hi POST / HTTP/1.1 Host: localhost:4567 Content-Length: 7 foo=bar HTTP/1.1 200 OK Content-Type: text/html;charset=utf-8 Content-Length: 3 Connection: keep-alive Server: thin 1.2.11 codename Bat-Shit Crazy Hi
Success! We now have a custom extension that allows us to respond to two verbs in one route without duplicating any code. The extension approach excels at handling low-level routing and configuration requirements deftly.
Helpers and extensions are something like cousins: you can
recognize them both as being from the same family, but they have quite
different roles to play. Instead of calling register
to let Sinatra know about them, you
pass them to helpers
. Most
importantly, they’re available both in the block you pass to your route
and the view template itself, making them effective across application
tiers.
Let’s look at an archetypical helper method: one that generates hyperlinks. The code is shown in Example 3-8.
require 'sinatra/base' module Sinatra module LinkHelper def link(name) case name when :about then '/about' when :index then '/index' else "/page/#{name}" end end helpers LinkHelper end
All you need to do is require
'./sinatra/link_helper'
in your main Sinatra application, and
you’ll be able to make use of the LinkHelper
module throughout. Let’s make a
quick view in Erb that tests it, demonstrated in Example 3-9.
<html> <head> <title>Link Helper Test</title> </head> <body> <nav> <ul> <li><a href="<%= link(:index) %>">Index</a></li> <li><a href="<%= link(:about) %>">About</a></li> <li><a href="<%= link(:random) %>">Random</a></li> </ul> </nav> </body> </html>
Our links are nicely rendered, and mousing over indicates they’re pointing to /index, /about, and /page/random as intended.
Sometimes you need to create a helper or two that are only going
to be used in one application or for a specific purpose. The helpers
method used in Example 3-10 accommodates this case by accepting a block,
avoiding the overhead of creating modules, and so on.
require 'sinatra' helpers do def link(name) case name when :about then '/about' when :index then '/index' else "/page/#{name}" end end get '/' do erb :index end get '/index.html' do redirect link(:index) end __END__ @@index <a href="<%= link :about %>">about</a>
What’s up with the funky @@index
stuff at the bottom of Example 3-10? It’s what Sinatra refers to as an
inline template. Got a small amount of HTML to
deliver and don’t want to create a whole view file dedicated to it?
You can provide it after your routing code and call it the same way
you would a normal view. Figure 3-3 shows
the rendered output of our friendly helpers.
What if you want to create an extension that ships with a helper
as well? Sinatra provides a hook for this type of activity via a method
called registered
. Simply create a
registered
method that takes the
application class as an argument. Example 3-11 provides an
example of how you might organize your methods; register them with
Sinatra as shown and it becomes trivial to produce some fairly sweeping
changes to how Sinatra executes.
The next step in understanding Sinatra’s internals is to examine the flow of a request, from parsing to delivery of a response back to the client. To do so, we need to examine the role of Rack (which we’ve mentioned briefly earlier) in the Sinatra landscape.
Rack is a specification implemented by not only Sinatra, but also
Rails, Merb, Ramaze, and a number of other Ruby projects. It’s an
extremely simple protocol specifying how an HTTP server (such as Thin,
which we’ve used throughout the book) interfaces with an application
object, like Sinatra::Application
,
without having to know anything about Sinatra in particular. In short,
Rack defines the higher-level vocabulary that hardware and software can
use to communicate. The Rack homepage, http://rack.rubyforge.org, is shown in Figure 3-4.
The Rack protocol at its core specifies that the application
object, the so-called endpoint, has to respond to
the method call
. The server, usually
referred to as the handler, will call that method
with one parameter. This parameter is a hash containing all relevant
information about the request: this includes the HTTP verb used by the
request, the path that is requested, the headers that have been sent by
the client, and so on.
The method is expected to return an array with three elements. The
first one is the status code, provided as an integer. For example, a
successful request may receive status code 200, indicating that no
errors occurred. The second element is a hash (or hash-like object in
Rack 1.3 or later) containing all the response headers. Here you’ll find
information such as whether or not the client should cache the response,
the length of the response, and similar information. The last object is
the body object. This object is required to behave
like an array of strings; that is, it has to respond to each
and call the passed block with
strings.
What’s neat about this is that it’s completely possible (and acceptable) to run a Sinatra application without truly invoking Sinatra. Let’s try to port a simple Sinatra application to pure Rack, shown in Example 3-12.
module MySinatra class Application def self.call(env) new.call(env) end def call(env) headers = {'Content-Type' => 'text/html'} if env['PATH_INFO'] == '/' status, body = 200, 'hi' else status, body = 404, "Sinatra doesn't know this ditty!" end headers['Content-Length'] = body.length.to_s [status, headers, [body]] end end end require 'thin' Thin::Server.start MySinatra::Application, 4567
Example 3-12 is roughly equivalent to get('/') { 'hi' }
. Of course, this is not the
implementation found in Sinatra, since the Sinatra implementation is
generic and handles a larger number of use cases, contains wrappers,
optimizations, and so on. Sinatra will, however, wrap the env
hash in a convenience object, available to
your code in the form of the request
object. Likewise, response
is
available for generating the body array. These are easily accessible in
your application; take a look at Example 3-13 to see how
they’re used.
require 'sinatra' helpers do def assert(condition) fail "something is terribly broken" unless condition end end get '/' do assert env['PATH_INFO'] == request.path_info final_result = response.finish assert Array === final_result assert final_result.length == 3 assert final_result.first == 200 "everything is fine" end
What does all this mean for us as Sinatra developers? If you hop
into IRB again and try typing Sinatra::Application.new.class
, you will find
that new
does not return an instance of
Sinatra::Application
(give it a shot;
it actually returns an instance of
Rack::MethodOverride
).
The Rack specification supports chaining filters and routers in
front of your application. In Rack slang, those are called
middleware. This middleware also implements the Rack
specification; it responds to call
and
returns an array as described above. Instead of simply creating that array
on its own, it will use different Rack endpoint or middleware and simply
call call
on that object. Now this
middleware can modify the request (the env
hash), modify the response, decide whether
or not to call the next endpoint, or any combination of those. By
returning a Rack::MethodOverride
object
instead of a Sinatra::Application
object, Sinatra respects this middleware chaining.
Rack has an additional specification for middleware. Middleware is created by a factory object. This object has to respond to new; new takes at least one argument, which is the endpoint that will be wrapped by the middleware. Finally, the middleware returns the wrapped endpoint.
Usually the factory is simply a class, like Sinatra::ShowException
, and the instances of
this class are the concrete middleware configurations, with a fixed
endpoint. Let’s set Sinatra aside for a moment and write a simple Rack
application again. We can use a Proc
object for that, since it responds to call
. We will also create a simple middleware
that will check if the path is correct.
The rack
gem should already be
installed on your system, since Sinatra depends on it. It comes with a
handy tool called rackup, which understands a
simple DSL for setting up a Rack application (you may recall we used a
rackup file in Chapter 1 to deploy code to Heroku). Create
a file called config.ru with the contents shown in
Example 3-14. Once you’ve done so, run rackup -p 4567 -s thin
from the same
directory. You should be able to view your application at http://localhost:4567/.
MyApp = proc do |env| [200, {'Content-Type' => 'text/plain'}, ['ok']] end class MyMiddleware def initialize(app) @app = app end def call(env) if env['PATH_INFO'] == '/' @app.call(env) else [404, {'Content-Type' => 'text/plain'}, ['not ok']] end end end # this is the actual configuration use MyMiddleware run MyApp
The features exposed by Rack are so handy that Sinatra actually
ships with a use
method that behaves
exactly like the version offered by rackup. Example 3-15 shows it in use.
require 'sinatra' require 'rack' # A handy middleware that ships with Rack # and sets the X-Runtime header use Rack::Runtime get('/') { 'Hello world!' }
Although interesting, the question lingers: how does this all connect to day-to-day development in Sinatra? The answer: you can use any Sinatra application as middleware.
The class, Sinatra::Application
, is the factory creating
the configured middleware instance (which is your application instance).
When the request comes in, all before filters are
triggered. Then, if a route matches, the corresponding block will be
executed. If no route matches, the request is handed off to the wrapped
application. The after filters are executed after
we’ve got a response back from the route or wrapped app. Thus, your
application is Rack middleware.
There is, however, one catch: Sinatra relies on the “one instance per request” principle. However, when running as middleware, all requests will use the same instance over and over again. Sinatra performs a clever trick here: instead of executing the logic right away, it duplicates the current instance and hands responsibility on to the duplicate instead. Since instance creation (especially with all the middleware being set up internally) is not free from a performance and resources standpoint, it uses that trick for all requests (even if running as endpoint) by keeping a prototype object around.
Example 3-16 shows the secret sauce in Sinatra’s dispatch activities.
module MySinatra class Base def self.prototype @prototype ||= new end def self.call(env) prototype.call(env) end def call(env) dup.call!(env) end def call!(env) [200, {'Content-Type' => 'text/plain'}, ['routing logic not implemented']] end end class Application < Base end end
This lets us craft some pretty interesting Sinatra applications.
This prototype and instance duplication approach means you can safely
use call
on the current instance and
consume the result of another route. If you remember from the earlier
discussion on Rack’s methodology, the call
method will return an array. The
application in Example 3-17 lets you check the status code
and headers of other routes. Figure 3-5 shows the
output of the inspector application.
require 'sinatra' get '/example' do 'go to /inspect/example' end get '/inspect/*' do route = "/" + params[:splat].first data = call env.merge("PATH_INFO" => route) result = "Status: #{data[0]} " data[1].each do |header, value| result << "#{header}: #{value} " end result << " " data[2].each do |line| result << line end content_type :txt result end
Now let’s tie everything together with the code in Example 3-18 and create a Sinatra application that acts as middleware.
To bring the discussion back to where we began, let’s focus on a
block passed to get again. How is it that the
instance methods are actually available? If you’ve been working with
Ruby for a decent length of time, you’ve probably come across instance_eval
, which allows you to dynamically
change the binding of a block. Example 3-19 demonstrates
how this can be used.
$ irb ruby-1.9.2-p180 > array = ['foo', 'bar'] => ['foo', 'bar'] ruby-1.9.2-p180 > block = proc { first } => #<Proc:0x00000101017c58@(irb):2> ruby-1.9.2-p180 > block.call NameError: undefined local variable or method `first' for main:Object from (irb):2:in `block in irb_binding' from (irb):3:in `call' from (irb):3 from /Users/konstantin/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `<main>' ruby-1.9.2-p180 > array.instance_eval(&block) => "foo"
This is similar to what Sinatra does. In fact, earlier versions of
Sinatra do use instance_eval
.
However, there is an alternative: dynamically create a method from that
block, get the unbound method object for that
method, and remove the method immediately. When you want to run the
code, bind the method object to the current instance and call it.
This has a few advantages over instance_eval
: it results in significantly
better performance since the scope change only occurs once as opposed to
every request. It also allows the passing of arguments to the block.
Moreover, since you can name the method yourself, it results in more
readable stack traces. All of this logic is wrapped in Sinatra’s
generate_method
, which you can
examine in Figure 3-6 and Example 3-20.
generate_method
is used
internally by Sinatra and is not part of the public API. You should
not use it directly in your application.
This has been a deep chapter! It’s certainly a lot to take in given how simple and straightforward Sinatra is on the surface. We have started by digging just a little deeper into Sinatra’s implementation details with every step in this chapter. By now, you should have a general understanding of what is going on, how the routing system works, and what Sinatra will do with the results.
We also introduced you to Rack in this chapter, which is the foundation for basically any and all Ruby web applications you’re likely to run across. Understanding Rack will also help you understand the internals of other Ruby web frameworks and libraries (such as Rails) or web servers (like Thin). Understanding how Sinatra and Rack tick will help us design cleaner and more powerful applications, and opens the doors from a creative architecture standpoint.
In Chapter 4, we will have a look into modular applications, which allows Sinatra to be an even better Rack citizen.