Chapter 3. A Peek Behind the Curtain

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.

Note

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/.

Application and Delegation

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?

Example 3-1. A simple script to check the availability of params
$ 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.

The Inner 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.

Example 3-2. Demonstrating the optional use of self
$ 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.

Example 3-3. Inspecting self in different scopes
require "sinatra"

outer_self = self
get '/' do
  content_type :txt
  "outer self: #{outer_self}, inner self: #{self}"
end

Note

Technically speaking, closures in Ruby open a new, nested scope. We’ll see more on this in a moment.

Examining the state of self in different scopes
Figure 3-1. Examining the state of self in different scopes

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.

Note

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.

Where Does get Come From?

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.

Example 3-4. Using reflection in IRB
[~]$ 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.

Note

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.

Example 3-5. Using reflection in IRB, continued
[~]$ 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

Exploring the Implementation

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.

Note

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.

Exploring Sinatra’s source on GitHub
Figure 3-2. Exploring Sinatra’s source on GitHub

Helpers and Extensions

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.

Note

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!

Creating Sinatra Extensions

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.

Example 3-6. Creating the Sinatra::PostGet extension
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.

Note

Once you’ve tried the extension and observed it functioning, try removing the register PostGet call from the module. What happens?

Example 3-7. Using custom Sinatra::PostGet extension
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

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.

Example 3-8. A helper method that generates hyperlinks
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.

Example 3-9. An Erb view to test the module
<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.

Helpers Without Modules

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.

Example 3-10. Creating a helper via a block
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>

Note

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.

Using the link helper module
Figure 3-3. Using the link helper module

Combining Helpers and Extensions

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.

Example 3-11. Combining helpers with extensions
require 'sinatra/base'
module MyExtension
  module Helpers
    # helper methods go here
  end

  # extension methods go here

  def self.registered(app)
    app.helpers Helpers
  end
end

Sinatra.register MyExtension

Request and Response

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

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.

You can learn more about Rack at
Figure 3-4. You can learn more about Rack at http://rack.rubyforge.org

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.

Sinatra Without Sinatra

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.

Example 3-12. Simplified equivalent of a Sinatra application using Rack
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.

Example 3-13. Using env, request, and response in Sinatra
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

Rack It Up

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.

Middleware

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/.

Example 3-14. Contents of config.ru
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

Sinatra and Middleware

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.

Example 3-15. Using use in Sinatra
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.

Dispatching

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.

Example 3-16. The Sinatra dispatch in action
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

Dispatching Redux

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.

Example 3-17. A reflective route inspector
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.

Example 3-18. Using Sinatra as middleware in a fictional Rails project
require './sinatra_middleware'
require './config/environment'

use Sinatra::Application
run MyRailsProject::Application
Firing another request internally to inspect the response
Figure 3-5. Firing another request internally to inspect the response

Changing Bindings

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.

Example 3-19. Toying with instance_eval
$ 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"
generate_method and its usage in Sinatra
Figure 3-6. generate_method and its usage in Sinatra

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.

Caution

generate_method is used internally by Sinatra and is not part of the public API. You should not use it directly in your application.

Example 3-20. generate_method from sinatra/base.rb
def generate_method(method_name, &block)
  define_method(method_name, &block)
  method = instance_method method_name
  remove_method method_name
  method
end

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset