back home
πŸ“š notes on the book: Sinatra: Up and Running Alan Harris, Konstantin Haase Everything you read here is taking from there.

The EntraΓ±as of Sinatra

Routes in sintara, like get '/home' are not method definitions. They are calls to methods deeper in the sinatra code base that receive the block of what the route is supposed to do.

The funny thing is that blocks in ruby are supposed to have the same variables, and methods the defining scope has. Meaning you can do something like this:


numbers = [1, 4, 5, 6]
other = 'smth'

numbers.each do |number|
    puts other # you can access the variable defined in the scope above
    puts number + 1
end

Opposed to that if you try to access params outside of a route block, it does not let you.


require 'sinatra'

get '/' do
  params
end

puts params # it will not run, since this is not defined.

But why is that? Well the secret is on how Sinatra manges self.

In Ruby all method calls that are not sent to a variable are sent to self. Therefore if you say puts is the same as saying self.puts.

The thing is self will change when you cross a "scope gate". Basically these are sections of code where the context changes, like class definitions, module definitions, methods, and so on.

When you enter the get '/home' do we are changing the context, and self becomes different.


require 'sinatra'

outer_self = self

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

The response is this:

outer self main, inner self: #

Note how self inside the block is pointing to Sinatra::Application.

So where is get defined? It is defined in two places, one is in Object, and the other one is in Sinatra::Base. Sinatra does this to be able to use this clean DSL where you just type get and ruby knows what to do.

But it is not defined in the actual code of Object since that would pollute it. Instead it uses a thing called mixins. There is a Sinatra::Delegator mixin, that is "defining" the method in Object. From there Object will send the call to Sinatra::Application who would finially call Sinatra:Base for the actual implementation.


Object (with Delegator mixin) -> Sinatra::Application -> Sinatra::Base
# forwards the call           -> inherits from         -> has implementation

All this to say that we can skip the DSL thing, and just require 'sinatra/base' in case we want to do modular applications or something that the Sinatra DSL does not natively do.

Helpers and Extensions

We can extend the DSL of Sinatra with extension, these are mostly used for configuration and routing.

Here is an example of an extension for a method that accepts both post and get requests. The project, looks something similar to this:


.
β”œβ”€β”€ extensions.rb
└── sinatra
    └── post_get.rb

post_get.rb


require 'sinatra/base'

module Sinatra
  module PostGet
    def post_get(route, &block)
      get(route, &block)
      post(route, &block)
    end
  end

  # registering it in Sinatra::Base
  register PostGet
end

Now inside the extensions.rb, we can use post_get as if it were defined in the delegator. Meaning as if it were a built-in native.


% cat extensions.rb
require 'sinatra'
require './sinatra/post_get'

post_get '/' do
  "hi #{params[:name]}"
end

This approach excels for low-level routing and configuration requirements.

We also have helpers. They play different role from extensions, they are available in the route block and in the views. Here is an archetypical example


.
β”œβ”€β”€ helpers.rb
└── sinatra
    β”œβ”€β”€ link_helper.rb

link_helper.rb


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
  end

  helpers LinkHelper # note we use helpers instead of register
end

helpers.rb


require 'sinatra'
require './sinatra/link_helper'

get '/' do
  erb :index
end

__END__

@index
<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>

As stated above, we can use :index, :about inside the view.

You can also put the helpers in a single file to avoid creating unnecessary modules.


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>

Combining Helpers and Extensions

You can create a module with your extension, and create a module inside that one with the helpers, then call a function registered that takes app as a param and add the helpers there.


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

Sinatra (and many other ruby web frameworks) uses Rack. This is a protocol that tells how an HTTP server interfaces with an application object.

Inside Rack you can define the application object, this need to have a call method that receives one parameter. The parameter (env) is a Hash that contains the info of the request. You then need to return an array with 3 things, the response code, the headers, and the body.

Sinatra wraps this env in the request object. Sinatra calls this "applications" middleware. You can stack them up and have different middleware do different stuff to the request, since they can modify it, or even stop it.

The thing about Sinatra is that it can act as one of this middleware. You can stack multiple apps together, have one app handle some routes and other other ones.