π notes on the book: Sinatra: Up and Running Alan Harris, Konstantin Haase Everything you read here is taking from there.
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.
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>
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
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.