Writing Extensions
Sinatra
includes an API for extension authors to help ensure that
consistent behavior is provided for application developers.
Background
Some knowledge of Sinatra's internal design is required to write good extensions. This section provides a high level overview of the classes and idioms at the core of Sinatra's design.
Sinatra
has two distinct modes of use that extensions should be aware of:
The "Classic" style, where applications are defined on main / the top-level -- most of the examples and documentation target this usage. Classic applications are often single-file, standalone apps that are run directly from the command line or with a minimal rackup file. When an extension is required in a classic application, the expectation is that all extension functionality should be present without additional setup on the application developers part (like included/extending modules).
The "Modular" style, where
::Sinatra::Base
is subclassed explicitly and the application is defined within the subclass's scope. These applications are often bundled as libraries and used as components within a larger Rack-based system. Modular applications must include any desired extensions explicitly by callingregister ExtensionModule
within the application's class scope.
Most extensions are relevant to both styles but care must be taken by extension authors to ensure that extensions do the right thing under each style. The extension API (Sinatra.register and Sinatra.helpers) is provided to help extension authors with this task.
Important: The following notes on ::Sinatra::Base
and
::Sinatra::Application
are provided for background only - extension
authors should not need to modify these classes directly.
::Sinatra::Base
The ::Sinatra::Base
class provides the context for all evaluation in a
Sinatra
application. The top-level DSLish stuff exists in class scope
while request-level stuff exists at instance scope.
Applications are defined within the class scope of a ::Sinatra::Base
subclass. The "DSL" (e.g., get
, post
, before
, configure
, set
,
etc.) is simply a set of class methods defined on ::Sinatra::Base
. Extending
the DSL is achieved by adding class methods to ::Sinatra::Base
or one of its
subclasses. However, Base classes should not be extended with
extend
; the Sinatra.register method (described below) is provided
for this task.
Requests are evaluated within a new ::Sinatra::Base
instance -- routes,
before filters, views, helpers, and error pages all share this same context.
The default set of request-level helper methods (e.g, erb
, haml
, halt
,
content_type
, etc.) are simple instance methods defined on ::Sinatra::Base
or within modules that are included in ::Sinatra::Base
. Providing new
functionality at the request level is achieved by adding instance methods to
::Sinatra::Base
.
As with DSL extensions, helper modules should not be added directly to
::Sinatra::Base
by extension authors with include
; the
Sinatra.helpers method (described below) is provided for this task.
::Sinatra::Application
The ::Sinatra::Application
class provides the default execution context
for classic style applications. It is a simple subclass of
::Sinatra::Base
that provides default option values and other behavior
tailored for top-level apps. When a classic style application is run,
all ::Sinatra::Application
public class methods are exported to the
top-level.
Rules for Extensions
Never modify
::Sinatra::Base
directly. You should not include or extend, change option values, or modify its behavior elsewise. Modular style applications will include your extension in their subclass explicitly using theregister
method.Never
require 'sinatra'
in your extension. You should only ever need torequire 'sinatra/base'
. The reason for this is thatrequire 'sinatra'
is what triggers the classic style -- extensions should never trigger the classic style.Use the APIs described below where possible. You should not need to include or extend modules directly or define methods directly on a core Sinatra class. The specialized methods below handle all of that for you.
Extensions should be defined in separate modules under the
Sinatra
module. For example, an extension that added basic authentication primitives might be namedSinatra::BasicAuth
.
Extending The Request Context with Sinatra.helpers
The most common type of extension is one that adds methods for use in routes, views, and helper methods.
For example, suppose you wanted to write an extension that added an h
method that escaped reserved HTML characters (such as those found in other
popular Ruby web frameworks).
require 'sinatra/base'
module Sinatra
module HTMLEscapeHelper
def h(text)
Rack::Utils.escape_html(text)
end
end
helpers HTMLEscapeHelper
end
The call to Sinatra.helpers includes the module in
::Sinatra::Application
, making all methods defined in the module
available to classic style applications. Using this extension in classic
style apps is as simple as requiring the extension and using the new
method:
require 'sinatra'
require 'sinatra/htmlescape'
get "/hello" do
h "1 < 2" # => "1 < 2"
end
::Sinatra::Base
subclasses, on the other hand, must require and include
the module explicitly using the helpers
method:
require 'sinatra/base'
require 'sinatra/htmlescape'
class HelloApp < Sinatra::Base
helpers Sinatra::HTMLEscapeHelper
get "/hello" do
h "1 < 2"
end
end
Extending The DSL (class) Context with Sinatra.register
Extensions can also extend Sinatra's class level DSL using the
Sinatra.register method. Here's an extension that adds a
block_links_from
macro that checks the referer on each request for
a app provided pattern and sends back a 403 Forbidden
response when
a match is detected:
require 'sinatra/base'
module Sinatra
module LinkBlocker
def block_links_from(host)
before {
halt 403, "Go Away!" if request.referer.match(host)
}
end
end
register LinkBlocker
end
Sinatra.register adds all public methods in the module(s) given as class
methods on ::Sinatra::Application
. It also handles exporting public methods to
the top-level when classic style apps are executed.
A classic style application would use this extension as follows:
require 'sinatra'
require 'sinatra/linkblocker'
block_links_from 'digg.com'
get '/' do
"Hello World"
end
Modular style applications must register the extension explicitly in their
::Sinatra::Base
subclasses:
require 'sinatra/base'
require 'sinatra/linkblocker'
class Hello < Sinatra::Base
register Sinatra::LinkBlocker
block_links_from 'digg.com'
get '/' do
"Hello World"
end
end
Setting Options and Other Extension Setup
Extensions can define options, routes, before filters, and error handlers by
defining a registered
method on the extension module. The Module.registered
method is called immediately after the extension module is added to the
::Sinatra::Base
subclass and is passed the class that the module was
registered with.
The following example creates a very simple extension that adds basic session auth support. Options are added for the username and password, routes are defined for logging in, and helper methods are provided for determining whether a user has been authorized:
require 'sinatra/base'
module Sinatra
module SessionAuth
module Helpers
def
session[: ]
end
def
redirect '/login' unless
end
def logout!
session[: ] = false
end
end
def self.registered(app)
app.helpers SessionAuth::Helpers
app.set :username, 'frank'
app.set :password, 'changeme'
app.get '/login' do
"<form method='POST' action='/login'>" +
"<input type='text' name='user'>" +
"<input type='text' name='pass'>" +
"</form>"
end
app.post '/login' do
if params[:user] == .username && params[:pass] == .password
session[: ] = true
redirect '/'
else
session[: ] = false
redirect '/login'
end
end
end
end
register SessionAuth
end
A classic application would use this extension by requiring the extension library, overriding options, and using the helpers provided:
require 'sinatra'
require 'sinatra/sessionauth'
set :password, 'hoboken'
get '/public' do
if
"Hi. I know you."
else
"Hi. We haven't met. <a href='/login'>Login, please.</a>"
end
end
get '/private' do
'Thanks for logging in.'
end
A modular application is different only in that it must register the extension module explicitly:
require 'sinatra/base'
require 'sinatra/sessionauth'
class MyApp < Sinatra::Base
register Sinatra::SessionAuth
set :password, 'hoboken'
get '/public' do
if
"Hi. I know you."
else
"Hi. We haven't met. <a href='/login'>Login, please.</a>"
end
end
get '/private' do
'Thanks for logging in.'
end
end
Building and Packaging Extensions
Sinatra extensions should be built as separate libraries and packaged as
gems or as single files that can be included within an application's lib
directory. The ideal process for using an extensions is installing a gem
and requiring a single file.
The following is an example file layout for a typical extension packaged as a gem:
sinatra-fu
|-- README
|-- LICENSE
|-- Rakefile
|-- lib
| `-- sinatra
| `-- fu.rb
|-- test
| `-- spec_sinatra_fu.rb
`-- sinatra-fu.gemspec