Metaprogramming in Sinatra: A Peek Under the Hood

While wrapping up the second week of Hacker School and finishing Constantin’s “Sinatra: Up and Running”, I decided to peer under the hood of Sinatra itself and investigate how the magic unfolded. I was quite pleased when I ran into one of my favourite Ruby black-magic methods: define_method.

Prima facie, it may rather deceptively appear that Sinatra’s basic HTTP route definitions (get ‘/’, post ‘/’, put ‘/’, delete ‘/’) are method definitions, but au contraire- they are method calls deep inside the Sinatra lair. Let’s take a peek.

1
2
3
get '/' do
  "bonjour, Neptune."
end

Inside this ‘get’ method call, self is actually an instance of the class “Sinatra::Application”, which means our routes are passing blocks to instances of “Sinatra::Application”.

But who, if we may entreat, is the parent of “Sinatra::Application”? Well, none other than “Sinatra::Base.

And who is the parent of Base? Object! (::Cue spooky voice:: And who, pray tell, is the parent of Object? BasicObject! But you knew that already, didn’t you, good Rubyist?)

Methods like our ‘get’, ‘post’, ‘put’, and ‘delete’ are actually defined twice; first in the module “Sinatra::Delegator”, as a mixin extending Object. Because Object is extended, the methods are now readily available at the top level, throughout our entire application. The module Sinatra::Delegator sends the method calls to Sinatra::Application, which inherits them from Sinatra::Base.

And now for a crash course in Ruby metaprogramming! Look at the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Prehistoric
  def initialize
    @dinosaurs = {
      "Stacy" => "platypus", # you know it!
      "Jenna" => "brontosaurus",
      "Madolyn" => "pterodactyl",
      "Casey" => "iguana",
      "Carolyn" => "t-rex"
      }
  end

# isn't this a bit tedious?
  def stacy; @dinosaurs["Stacy"]; end
  def jenna; @dinosaurs["Jenna"]; end
  def madolyn; @dinosaurs["Madolyn"]; end

# Ruby Tuesday shortcut with 'define\_method'
  %w( stacy jenna madolyn casey carolyn ).each do |method|
    define_method(method) { @dinosaurs[method.capitalize] }
  end
end

Pulling up Sinatra’s main.rb file:

licentiously lifted from (main.rb)
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
require 'sinatra/base'

module Sinatra
  class Application < Base

    # we assume that the first file that requires 'sinatra' is the
    # app_file. all other path related options are calculated based
    # on this path by default.
    set :app_file, caller_files.first || $0

    set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }

    if run? && ARGV.any?
      require 'optparse'
      OptionParser.new { |op|
        op.on('-p port',   'set the port (default is 4567)')                { |val| set :port, Integer(val) }
        op.on('-o addr',   'set the host (default is 0.0.0.0)')             { |val| set :bind, val }
        op.on('-e env',    'set the environment (default is development)')  { |val| set :environment, val.to_sym }
        op.on('-s server', 'specify rack server/handler (default is thin)') { |val| set :server, val }
        op.on('-x',        'turn on the mutex lock (default is off)')       {       set :lock, true }
      }.parse!(ARGV.dup)
    end
  end

  at_exit { Application.run! if $!.nil? && Application.run? }
end

include Sinatra::Delegator

‘main.rb’ simply creates the Sinatra module, which is just subclassed from Sinatra::Application from Sinatra::Base. By the way, isn’t it neat to see all the options available for our disposal (changing ports!!!)? Hm, see the include Sinatra::Delegator mixin at the end? The Delegator mixin wields most of the magic in Sinatra…

And if we may allow ourselves to politely peek under the covers of Delegator:

sleazily excerpted from (base.rb)
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
# Sinatra delegation mixin. Mixing this module into an object causes all
# methods to be delegated to the Sinatra::Application class. Used primarily
# at the top-level.
module Delegator #:nodoc:
  def self.delegate(*methods)
    methods.each do |method_name|
      define_method(method_name) do |*args, &block|
        return super(*args, &block) if respond_to? method_name
        Delegator.target.send(method_name, *args, &block)
      end
      private method_name
    end
  end

  delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
           :before, :after, :error, :not_found, :configure, :set, :mime_type,
           :enable, :disable, :use, :development?, :test?, :production?,
           :helpers, :settings

  class << self
    attr_accessor :target
  end

  self.target = Application
end

So, how does ROUTING essentially work?

When unsuspecting websurfer Rachel Rabblerouser visits our app at “http://our_ineffably_splendiferous_app.com/”, her browser (Omniweb, because she’s a l337 hacker too cool for school Firefox) issues an HTTP ‘GET’ request for the resource (our website), our instance of Sinatra will take heed of the HTTP verb + path + code block (ie. get '/' do; "buy me falafels, please"; end), register this in Application, and accordingly- execute it..

Comments