mirror of
https://github.com/sinatra/sinatra
synced 2023-03-27 23:18:01 -04:00
56bcd4ca6c
Signed-off-by: Konstantin Haase <konstantin.mailinglists@googlemail.com>
1945 lines
55 KiB
Text
1945 lines
55 KiB
Text
= Sinatra
|
|
<i>Atención: Este documento es una traducción de la versión en inglés y puede estar desactualizado.</i>
|
|
|
|
Sinatra es un DSL para crear aplicaciones web rápidamente en Ruby con un mínimo
|
|
esfuerzo:
|
|
|
|
# miapp.rb
|
|
require 'sinatra'
|
|
|
|
get '/' do
|
|
'Hola mundo!'
|
|
end
|
|
|
|
Instalá la gem y ejecutá la aplicación con:
|
|
|
|
gem install sinatra
|
|
ruby -rubygems miapp.rb
|
|
|
|
Podés verla en: http://localhost:4567
|
|
|
|
Es recomendable además ejecutar <tt>gem install thin</tt>, ya que Sinatra lo va
|
|
a utilizar cuando esté disponible.
|
|
|
|
== Rutas
|
|
|
|
En Sinatra, una ruta está compuesta por un método HTTP y un patrón de una URL.
|
|
Cada ruta se asocia con un bloque:
|
|
|
|
get '/' do
|
|
.. mostrar algo ..
|
|
end
|
|
|
|
post '/' do
|
|
.. crear algo ..
|
|
end
|
|
|
|
put '/' do
|
|
.. reemplazar algo ..
|
|
end
|
|
|
|
patch '/' do
|
|
.. modificar algo ..
|
|
end
|
|
|
|
delete '/' do
|
|
.. aniquilar algo ..
|
|
end
|
|
|
|
options '/' do
|
|
.. informar algo ..
|
|
end
|
|
|
|
|
|
Las rutas son comparadas en el orden en el que son definidas. La primer ruta
|
|
que coincide con la petición es invocada.
|
|
|
|
Los patrones de las rutas pueden incluir parámetros nombrados, accesibles a
|
|
través de el hash <tt>params</tt>:
|
|
|
|
get '/hola/:nombre' do
|
|
# coincide con "GET /hola/foo" y "GET /hola/bar"
|
|
# params[:nombre] es 'foo' o 'bar'
|
|
"Hola #{params[:nombre]}!"
|
|
end
|
|
|
|
También podés acceder a los parámetros nombrados usando parámetros de bloque:
|
|
|
|
get '/hola/:nombre' do |n|
|
|
"Hola #{n}!"
|
|
end
|
|
|
|
Los patrones de ruta también pueden incluir parámetros splat (o wildcard),
|
|
accesibles a través del arreglo <tt>params[:splat]</tt>:
|
|
|
|
get '/decir/*/al/*' do
|
|
# coincide con /decir/hola/al/mundo
|
|
params[:splat] # => ["hola", "mundo"]
|
|
end
|
|
|
|
get '/descargar/*.*' do
|
|
# coincide con /descargar/path/al/archivo.xml
|
|
params[:splat] # => ["path/al/archivo", "xml"]
|
|
end
|
|
|
|
Rutas con Expresiones Regulares:
|
|
|
|
get %r{/hola/([\w]+)} do
|
|
"Hola, #{params[:captures].first}!"
|
|
end
|
|
|
|
O con un parámetro de bloque:
|
|
|
|
get %r{/hola/([\w]+)} do |c|
|
|
"Hola, #{c}!"
|
|
end
|
|
|
|
=== Condiciones
|
|
|
|
Las rutas pueden incluir una variedad de condiciones de selección, como por
|
|
ejemplo el user agent:
|
|
|
|
get '/foo', :agent => /Songbird (\d\.\d)[\d\/]*?/ do
|
|
"Estás usando la versión de Songbird #{params[:agent][0]}"
|
|
end
|
|
|
|
get '/foo' do
|
|
# Coincide con browsers que no sean songbird
|
|
end
|
|
|
|
Otras condiciones disponibles son +host_name+ y +provides+:
|
|
|
|
get '/', :host_name => /^admin\./ do
|
|
"Área de Administración, Acceso denegado!"
|
|
end
|
|
|
|
get '/', :provides => 'html' do
|
|
haml :index
|
|
end
|
|
|
|
get '/', :provides => ['rss', 'atom', 'xml'] do
|
|
builder :feed
|
|
end
|
|
|
|
Podés definir tus propias condiciones fácilmente:
|
|
|
|
set(:probabilidad) { |valor| condition { rand <= valor } }
|
|
|
|
get '/gana_un_auto', :probabilidad => 0.1 do
|
|
"Ganaste!"
|
|
end
|
|
|
|
get '/gana_un_auto' do
|
|
"Lo siento, perdiste."
|
|
end
|
|
|
|
=== Valores de Retorno
|
|
|
|
El valor de retorno de un bloque de ruta determina al menos el cuerpo de la
|
|
respuesta que se le pasa al cliente HTTP o al siguiente middleware en la pila
|
|
de Rack. Lo más común es que sea un string, como en los ejemplos anteriores.
|
|
Sin embargo, otros valor también son aceptados.
|
|
|
|
Podés devolver cualquier objeto que sea una respuesta Rack válida, un objeto
|
|
que represente el cuerpo de una respuesta Rack o un código de estado HTTP:
|
|
|
|
* Un arreglo con tres elementos: <tt>[estado (Fixnum), cabeceras (Hash), cuerpo de la respuesta (responde a #each)]</tt>
|
|
* Un arreglo con dos elementos: <tt>[estado (Fixnum), cuerpo de la respuesta (responde a #each)]</tt>
|
|
* Un objeto que responde a <tt>#each</tt> y que le pasa únicamente strings al bloque dado
|
|
* Un Fixnum representando el código de estado
|
|
|
|
De esa manera podemos, por ejemplo, implementar fácilmente un streaming:
|
|
|
|
class Stream
|
|
def each
|
|
100.times { |i| yield "#{i}\n" }
|
|
end
|
|
end
|
|
|
|
get('/') { Stream.new }
|
|
|
|
=== Comparadores de Rutas Personalizados
|
|
|
|
Como se mostró anteriormente, Sinatra permite utilizar Strings y expresiones
|
|
regulares para definir las rutas. Sin embargo, la cosa no termina ahí. Podés
|
|
definir tus propios comparadores muy fácilmente:
|
|
|
|
class PattronCualquieraMenos
|
|
Match = Struct.new(:captures)
|
|
|
|
def initialize(excepto)
|
|
@excepto = excepto
|
|
@capturas = Match.new([])
|
|
end
|
|
|
|
def match(str)
|
|
@capturas unless @excepto === str
|
|
end
|
|
end
|
|
|
|
def cualquiera_menos(patron)
|
|
PatronCualquieraMenos.new(patron)
|
|
end
|
|
|
|
get cualquiera_menos("/index") do
|
|
# ...
|
|
end
|
|
|
|
Tené en cuenta que el ejemplo anterior es un poco rebuscado. Un resultado
|
|
similar puede conseguirse más sencillamente:
|
|
|
|
get // do
|
|
pass if request.path_info == "/index"
|
|
# ...
|
|
end
|
|
|
|
O, usando un lookahead negativo:
|
|
|
|
get %r{^(?!/index$)} do
|
|
# ...
|
|
end
|
|
|
|
== Archivos Estáticos
|
|
|
|
Los archivos estáticos son servidos desde el directorio público
|
|
<tt>./public</tt>. Podés especificar una ubicación diferente ajustando la
|
|
opción <tt>:public</tt>:
|
|
|
|
set :public, File.dirname(__FILE__) + '/estaticos'
|
|
|
|
Notá que el nombre del directorio público no está incluido en la URL. Por
|
|
ejemplo, el archivo <tt>./public/css/style.css</tt> se accede a través de
|
|
<tt>http://ejemplo.com/css/style.css</tt>.
|
|
|
|
== Vistas / Plantillas
|
|
|
|
Se asume que las plantillas están ubicadas directamente bajo el directorio
|
|
<tt>./views</tt>. Para usar un directorio de vistas diferente:
|
|
|
|
set :views, File.dirname(__FILE__) + '/plantillas'
|
|
|
|
Es importante acordarse que siempre tenés que referenciar a las plantillas con
|
|
símbolos, incluso cuando se encuentran en un subdirectorio (en este caso tenés
|
|
que usar <tt>:'subdir/plantilla'</tt>). Tenés que usar un símbolo porque los
|
|
métodos de renderización van a renderizar directamente cualquier string que se
|
|
les pase como argumento.
|
|
|
|
=== Plantillas Haml
|
|
|
|
La gem/librería <tt>haml</tt> es necesaria para para renderizar plantillas HAML:
|
|
|
|
# Vas a necesitar requerir haml en tu app
|
|
require 'haml'
|
|
|
|
get '/' do
|
|
haml :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.haml</tt>.
|
|
|
|
Las {opciones de Haml}[http://haml-lang.com/docs/yardoc/file.HAML_REFERENCE.html#options]
|
|
pueden ser ajustadas globalmente a través de las configuraciones de Sinatra,
|
|
ver {Opciones y Configuraciones}[http://www.sinatrarb.com/configuration.html],
|
|
y reemplazadas individualmente.
|
|
|
|
set :haml, :format => :html5 # el formato por defecto de Haml es :xhtml
|
|
|
|
get '/' do
|
|
haml :index, :format => :html4 # reemplazado
|
|
end
|
|
|
|
=== Plantillas Erb
|
|
|
|
# Vas a necesitar requerir erb en tu app
|
|
require 'erb'
|
|
|
|
get '/' do
|
|
erb :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.erb</tt>
|
|
|
|
=== Plantillas Erubis
|
|
|
|
La gem/librería <tt>erubis</tt> es necesaria para renderizar plantillas Erubis:
|
|
|
|
# Vas a necesitar requerir erubis en tu app
|
|
require 'erubis'
|
|
|
|
get '/' do
|
|
erubis :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.erubis</tt>.
|
|
|
|
También es posible reemplazar Erb con Erubis:
|
|
|
|
require 'erubis'
|
|
Tilt.register :erb, Tilt[:erubis]
|
|
|
|
get '/' do
|
|
erb :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.erb</tt> con Erubis.
|
|
|
|
=== Plantillas Builder
|
|
|
|
La gem/librería <tt>builder</tt> es necesaria para renderizar plantillas
|
|
builder:
|
|
|
|
# Vas a necesitar requerir builder en tu app
|
|
require 'builder'
|
|
|
|
get '/' do
|
|
builder :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.builder</tt>.
|
|
|
|
=== Plantillas Nokogiri
|
|
|
|
La gem/librería <tt>nokogiri</tt> es necesaria para renderizar plantillas
|
|
nokogiri:
|
|
|
|
# Vas a necesitar requerir nokogiri en tu app
|
|
require 'nokogiri'
|
|
|
|
get '/' do
|
|
nokogiri :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.nokogiri</tt>.
|
|
|
|
=== Plantillas Sass
|
|
|
|
La gem/librería <tt>haml</tt> o <tt>sass</tt> es necesaria para renderizar
|
|
plantillas Sass:
|
|
|
|
# Vas a necesitar requerir haml o sass en tu app
|
|
require 'sass'
|
|
|
|
get '/stylesheet.css' do
|
|
sass :stylesheet
|
|
end
|
|
|
|
Renderiza <tt>./views/stylesheet.sass</tt>.
|
|
|
|
Las {opciones de Sass}[http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#options]
|
|
pueden ser ajustadas globalmente a través de las configuraciones de Sinatra,
|
|
ver {Opciones y Configuraciones}[http://www.sinatrarb.com/configuration.html],
|
|
y reemplazadas individualmente.
|
|
|
|
set :sass, :style => :compact # el estilo por defecto de Sass es :nested
|
|
|
|
get '/stylesheet.css' do
|
|
sass :stylesheet, :style => :expanded # reemplazado
|
|
end
|
|
|
|
=== Plantillas Scss
|
|
|
|
La gem/librería <tt>haml</tt> o <tt>sass</tt>es necesaria para renderizar
|
|
plantillas Scss:
|
|
|
|
# Vas a necesitar requerir haml o sass en tu app
|
|
require 'sass'
|
|
|
|
get '/stylesheet.css' do
|
|
scss :stylesheet
|
|
end
|
|
|
|
Renderiza <tt>./views/stylesheet.scss</tt>.
|
|
|
|
Las {opciones de Scss}[http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#options]
|
|
pueden ser ajustadas globalmente a través de las configuraciones de Sinatra,
|
|
ver {Opciones y Configuraciones}[http://www.sinatrarb.com/configuration.html],
|
|
y reemplazadas individualmente.
|
|
|
|
set :scss, :style => :compact # el estilo por defecto de Sass es :nested
|
|
|
|
get '/stylesheet.css' do
|
|
scss :stylesheet, :style => :expanded # reemplazado
|
|
end
|
|
|
|
=== Plantillas Less
|
|
|
|
La gem/librería <tt>less</tt> es necesaria para renderizar plantillas Less:
|
|
|
|
# Vas a necesitar requerir less en tu app
|
|
require 'less'
|
|
|
|
get '/stylesheet.css' do
|
|
less :stylesheet
|
|
end
|
|
|
|
Renderiza <tt>./views/stylesheet.less</tt>.
|
|
|
|
=== Plantillas Liquid
|
|
|
|
La gem/librería <tt>liquid</tt> es necesaria para renderizar plantillas Liquid:
|
|
|
|
# Vas a necesitar requerir liquid en tu app
|
|
require 'liquid'
|
|
|
|
get '/' do
|
|
liquid :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.liquid</tt>.
|
|
|
|
Como no vas a poder llamar a métodos de Ruby (excepto a +yield+) desde una
|
|
plantilla Liquid, casi siempre vas a querer pasarle locales:
|
|
|
|
liquid :index, :locals => { :clave => 'valor' }
|
|
|
|
=== Plantillas Markdown
|
|
|
|
La gem/librería <tt>rdiscount</tt> es necesaria para renderizar plantillas
|
|
Markdown:
|
|
|
|
# Vas a necesitar requerir rdiscount en tu app
|
|
require "rdiscount"
|
|
|
|
get '/' do
|
|
markdown :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.markdown</tt> (+md+ y +mkd+ también son extensiones
|
|
de archivo válidas).
|
|
|
|
No es posible llamar métodos desde markdown, ni pasarle locales. Por lo tanto,
|
|
generalmente vas a usarlo en combinación con otro motor de renderizado:
|
|
|
|
erb :resumen, :locals => { :texto => markdown(:introduccion) }
|
|
|
|
Tené en cuenta que también podés llamar al método +markdown+ desde otras
|
|
plantillas:
|
|
|
|
%h1 Hola Desde Haml!
|
|
%p= markdown(:saludos)
|
|
|
|
Como no podés utilizar Ruby desde Markdown, no podés usar layouts escritos en
|
|
Markdown. De todos modos, es posible usar un motor de renderizado para el
|
|
layout distinto al de la plantilla pasando la opción <tt>:layout_engine</tt>:
|
|
|
|
get '/' do
|
|
markdown :index, :layout_engine => :erb
|
|
end
|
|
|
|
Renderiza <tt>./views/index.md</tt> con el layout <tt>./views/layout.erb</tt>.
|
|
|
|
Recordá que podés asignar las opciones de renderizado globalmente:
|
|
|
|
set :markdown, :layout_engine => :haml, :layout => :post
|
|
|
|
get '/' do
|
|
markdown :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.md</tt> (o cualquier otra plantilla Markdown) con
|
|
el layout <tt>./views/post.haml</tt>.
|
|
|
|
También es posible parsear Markdown con BlueCloth en lugar de RDiscount:
|
|
|
|
require 'bluecloth'
|
|
|
|
Tilt.register 'markdown', BlueClothTemplate
|
|
Tilt.register 'mkd', BlueClothTemplate
|
|
Tilt.register 'md', BlueClothTemplate
|
|
|
|
get '/' do
|
|
markdown :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.md</tt> con BlueCloth.
|
|
|
|
=== Plantillas Textile
|
|
|
|
La gem/librería <tt>RedCloth</tt> es necesaria para renderizar plantillas
|
|
Textile:
|
|
|
|
# Vas a necesitar requerir redcloth en tu app
|
|
require "redcloth"
|
|
|
|
get '/' do
|
|
textile :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.textile</tt>.
|
|
|
|
No es posible llamar métodos desde textile, ni pasarle locales. Por lo tanto,
|
|
generalmente vas a usarlo en combinación con otro motor de renderizado:
|
|
|
|
erb :resumen, :locals => { :texto => textile(:introduccion) }
|
|
|
|
Tené en cuenta que también podés llamar al método +textile+ desde otras
|
|
plantillas:
|
|
|
|
%h1 Hola Desde Haml!
|
|
%p= textile(:saludos)
|
|
|
|
Como no podés utilizar Ruby desde Textile, no podés usar layouts escritos en
|
|
Textile. De todos modos, es posible usar un motor de renderizado para el
|
|
layout distinto al de la plantilla pasando la opción <tt>:layout_engine</tt>:
|
|
|
|
get '/' do
|
|
textile :index, :layout_engine => :erb
|
|
end
|
|
|
|
Renderiza <tt>./views/index.textile</tt> con el layout
|
|
<tt>./views/layout.erb</tt>.
|
|
|
|
Recordá que podés asignar las opciones de renderizado globalmente:
|
|
|
|
set :textile, :layout_engine => :haml, :layout => :post
|
|
|
|
get '/' do
|
|
textile :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.textile</tt> (o cualquier otra plantilla Textile)
|
|
con el layout <tt>./views/post.haml</tt>.
|
|
|
|
=== Plantillas RDoc
|
|
|
|
La gem/librería <tt>rdoc</tt> es necesaria para renderizar plantillas RDoc:
|
|
|
|
# Vas a necesitar requerir rdoc/markup/to_html en tu app
|
|
require "rdoc/markup/to_html"
|
|
|
|
get '/' do
|
|
rdoc :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.rdoc</tt>.
|
|
|
|
No es posible llamar métodos desde rdoc, ni pasarle locales. Por lo tanto,
|
|
generalmente vas a usarlo en combinación con otro motor de renderizado:
|
|
|
|
erb :resumen, :locals => { :texto => rdoc(:introduccion) }
|
|
|
|
Tené en cuenta que también podés llamar al método +rdoc+ desde otras
|
|
plantillas:
|
|
|
|
%h1 Hola Desde Haml!
|
|
%p= rdoc(:saludos)
|
|
|
|
Como no podés utilizar Ruby desde RDoc, no podés usar layouts escritos en RDoc.
|
|
De todos modos, es posible usar un motor de renderizado para el layout distinto
|
|
al de la plantilla pasando la opción <tt>:layout_engine</tt>:
|
|
|
|
get '/' do
|
|
rdoc :index, :layout_engine => :erb
|
|
end
|
|
|
|
Renderiza <tt>./views/index.rdoc</tt> con el layout <tt>./views/layout.erb</tt>.
|
|
|
|
Recordá que podés asignar las opciones de renderizado globalmente:
|
|
|
|
set :rdoc, :layout_engine => :haml, :layout => :post
|
|
|
|
get '/' do
|
|
rdoc :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.rdoc</tt> (o cualquier otra plantilla RDoc) con el
|
|
layout <tt>./views/post.haml</tt>.
|
|
|
|
=== Plantillas Radius
|
|
|
|
La gem/librería <tt>radius</tt> es necesaria para renderizar plantillas Radius:
|
|
|
|
# Vas a necesitar requerir radius en tu app
|
|
require 'radius'
|
|
|
|
get '/' do
|
|
radius :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.radius</tt>.
|
|
|
|
Como no vas a poder llamar a métodos de Ruby (excepto a +yield+) desde una
|
|
plantilla Radius, casi siempre vas a querer pasarle locales:
|
|
|
|
radius :index, :locals => { :clave => 'valor' }
|
|
|
|
=== Plantillas Markaby
|
|
|
|
La gem/librería <tt>markaby</tt> es necesaria para renderizar plantillas
|
|
Markaby:
|
|
|
|
# Vas a necesitar requerir markaby en tu app
|
|
require 'markaby'
|
|
|
|
get '/' do
|
|
markaby :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.mab</tt>.
|
|
|
|
También podés usar Markaby inline:
|
|
|
|
get '/' do
|
|
markaby { h1 "Bienvenido!" }
|
|
end
|
|
|
|
|
|
=== Plantillas Slim
|
|
|
|
La gem/librería <tt>slim</tt> es necesaria para renderizar plantillas Slim:
|
|
|
|
# Vas a necesitar requerir slim en tu app
|
|
require 'slim'
|
|
|
|
get '/' do
|
|
slim :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.slim</tt>.
|
|
|
|
=== Plantillas CoffeeScript
|
|
|
|
La gem/librería <tt>coffee-script</tt> y al menos <b>una</b> de las siguientes
|
|
opciones para ejecutar JavaScript:
|
|
|
|
* +node+ (de Node.js) en tu path
|
|
* utilizar OSX
|
|
* la gem/librería +therubyracer+
|
|
|
|
son necesarios para renderizar plantillas CoffeeScript:
|
|
|
|
# Vas a necesitar requerir coffee-script en tu app
|
|
require 'coffee-script'
|
|
|
|
get '/application.js' do
|
|
coffee :application
|
|
end
|
|
|
|
Renderiza <tt>./views/application.coffee</tt>.
|
|
|
|
=== Plantillas Embebidas
|
|
|
|
get '/' do
|
|
haml '%div.titulo Hola Mundo'
|
|
end
|
|
|
|
Renderiza el template embebido en el string.
|
|
|
|
=== Accediendo a Variables en Plantillas
|
|
|
|
Las plantillas son evaluadas dentro del mismo contexto que los manejadores de
|
|
ruta. Las variables de instancia asignadas en los manejadores de ruta son
|
|
accesibles directamente por las plantillas:
|
|
|
|
get '/:id' do
|
|
@foo = Foo.find(params[:id])
|
|
haml '%h1= @foo.nombre'
|
|
end
|
|
|
|
O es posible especificar un Hash de variables locales explícitamente:
|
|
|
|
get '/:id' do
|
|
foo = Foo.find(params[:id])
|
|
haml '%h1= foo.nombre', :locals => { :foo => foo }
|
|
end
|
|
|
|
Esto es usado típicamente cuando se renderizan plantillas como parciales desde
|
|
adentro de otras plantillas.
|
|
|
|
=== Plantillas Inline
|
|
|
|
Las plantillas pueden ser definidas al final del archivo fuente:
|
|
|
|
require 'rubygems'
|
|
require 'sinatra'
|
|
|
|
get '/' do
|
|
haml :index
|
|
end
|
|
|
|
__END__
|
|
|
|
@@ layout
|
|
%html
|
|
= yield
|
|
|
|
@@ index
|
|
%div.titulo Hola mundo!!!!!
|
|
|
|
NOTA: únicamente las plantillas inline definidas en el archivo fuente que
|
|
requiere sinatra son cargadas automáticamente. Llamá <tt>enable
|
|
:inline_templates</tt> explícitamente si tenés plantillas inline en otros
|
|
archivos fuente.
|
|
|
|
=== Plantillas Nombradas
|
|
|
|
Las plantillas también pueden ser definidas usando el método top-level
|
|
<tt>template</tt>:
|
|
|
|
template :layout do
|
|
"%html\n =yield\n"
|
|
end
|
|
|
|
template :index do
|
|
'%div.titulo Hola Mundo!'
|
|
end
|
|
|
|
get '/' do
|
|
haml :index
|
|
end
|
|
|
|
Si existe una plantilla con el nombre "layout", va a ser usada cada vez que
|
|
una plantilla es renderizada. Podés desactivar los layouts individualmente
|
|
pasando <tt>:layout => false</tt> o globalmente con
|
|
<tt>set :haml, :layout => false</tt>:
|
|
|
|
get '/' do
|
|
haml :index, :layout => !request.xhr?
|
|
end
|
|
|
|
=== Asociando Extensiones de Archivo
|
|
|
|
Para asociar una extensión de archivo con un motor de renderizado, usá
|
|
<tt>Tilt.register</tt>. Por ejemplo, si querés usar la extensión +tt+ para
|
|
las plantillas Textile, podés hacer lo siguiente:
|
|
|
|
Tilt.register :tt, Tilt[:textile]
|
|
|
|
=== Agregando Tu Propio Motor de Renderizado
|
|
|
|
Primero, registrá tu motor con Tilt, y después, creá tu método de renderizado:
|
|
|
|
Tilt.register :mipg, MiMotorParaPlantillaGenial
|
|
|
|
helpers do
|
|
def mypg(*args) render(:mypg, *args) end
|
|
end
|
|
|
|
get '/' do
|
|
mypg :index
|
|
end
|
|
|
|
Renderiza <tt>./views/index.mypg</tt>. Mirá https://github.com/rtomayko/tilt
|
|
para aprender más de Tilt.
|
|
|
|
== Filtros
|
|
|
|
Los filtros +before+ son evaluados antes de cada petición dentro del mismo
|
|
contexto que las rutas. Pueden modificar la petición y la respuesta. Las
|
|
variables de instancia asignadas en los filtros son accesibles por las rutas y
|
|
las plantillas:
|
|
|
|
before do
|
|
@nota = 'Hey!'
|
|
request.path_info = '/foo/bar/baz'
|
|
end
|
|
|
|
get '/foo/*' do
|
|
@nota #=> 'Hey!'
|
|
params[:splat] #=> 'bar/baz'
|
|
end
|
|
|
|
Los filtros +after+ son evaluados después de cada petición dentro del mismo
|
|
contexto y también pueden modificar la petición y la respuesta. Las variables
|
|
de instancia asignadas en los filtros +before+ y en las rutas son accesibles por
|
|
los filtros +after+:
|
|
|
|
after do
|
|
puts response.status
|
|
end
|
|
|
|
Nota: A menos que usés el método +body+ en lugar de simplemente devolver un
|
|
string desde una ruta, el cuerpo de la respuesta no va a estar disponible en
|
|
un filtro after, debido a que todavía no se ha generado.
|
|
|
|
Los filtros aceptan un patrón opcional, que cuando está presente causa que los
|
|
mismos sean evaluados únicamente si el path de la petición coincide con ese
|
|
patrón:
|
|
|
|
before '/protegido/*' do
|
|
autenticar!
|
|
end
|
|
|
|
after '/crear/:slug' do |slug|
|
|
session[:ultimo_slug] = slug
|
|
end
|
|
|
|
Al igual que las rutas, los filtros también pueden aceptar condiciones:
|
|
|
|
before :agent => /Songbird/ do
|
|
# ...
|
|
end
|
|
|
|
after '/blog/*', :host_name => 'ejemplo.com' do
|
|
# ...
|
|
end
|
|
|
|
== Ayudantes
|
|
|
|
Usá el método top-level <tt>helpers</tt> para definir métodos ayudantes que
|
|
pueden ser utilizados dentro de los manejadores de rutas y las plantillas:
|
|
|
|
helpers do
|
|
def bar(nombre)
|
|
"#{nombre}bar"
|
|
end
|
|
end
|
|
|
|
get '/:nombre' do
|
|
bar(params[:nombre])
|
|
end
|
|
|
|
=== Usando Sesiones
|
|
|
|
Una sesión es usada para mantener el estado a través de distintas peticiones.
|
|
Cuando están activadas, tenés un hash de sesión para cada sesión de usuario:
|
|
|
|
enable :sessions
|
|
|
|
get '/' do
|
|
"valor = " << session[:valor].inspect
|
|
end
|
|
|
|
get '/:valor' do
|
|
session[:valor] = params[:valor]
|
|
end
|
|
|
|
Tené en cuenta que <tt>enable :sessions</tt> guarda todos los datos en una
|
|
cookie, lo que no es siempre deseable (guardar muchos datos va a incrementar
|
|
tu tráfico, por citar un ejemplo). Podés usar cualquier middleware Rack para
|
|
manejar sesiones, de la misma manera que usarías cualquier otro middleware,
|
|
pero con la salvedad de que *no* tenés que llamar a <tt>enable :sessions</tt>:
|
|
|
|
use Rack::Session::Pool, :expire_after => 2592000
|
|
|
|
get '/' do
|
|
"valor = " << session[:valor].inspect
|
|
end
|
|
|
|
get '/:valor' do
|
|
session[:valor] = params[:valor]
|
|
end
|
|
|
|
Para incrementar la seguridad, los datos de la sesión almacenados en
|
|
la cookie son firmados con un secreto de sesión. Este secreto, es
|
|
generado aleatoriamente por Sinatra. De cualquier manera, hay que
|
|
tener en cuenta que cada vez que inicies la aplicación se va a generar
|
|
uno nuevo. Así, si querés que todas las instancias de tu aplicación
|
|
compartan un único secreto, tenés que definirlo vos:
|
|
|
|
set :session_secret, 'super secreto'
|
|
|
|
Si necesitás una configuración más específica, +sessions+ acepta un
|
|
Hash con opciones:
|
|
|
|
set :sessions, :domain => 'foo.com'
|
|
|
|
=== Interrupción
|
|
|
|
Para detener inmediatamente una petición dentro de un filtro o una ruta usá:
|
|
|
|
halt
|
|
|
|
También podés especificar el estado:
|
|
|
|
halt 410
|
|
|
|
O el cuerpo:
|
|
|
|
halt 'esto va a ser el cuerpo'
|
|
|
|
O los dos:
|
|
|
|
halt 401, 'salí de acá!'
|
|
|
|
Con cabeceras:
|
|
|
|
halt 402, { 'Content-Type' => 'text/plain' }, 'venganza'
|
|
|
|
Obviamente, es posible utilizar +halt+ con una plantilla:
|
|
|
|
halt erb(:error)
|
|
|
|
=== Paso
|
|
|
|
Una ruta puede pasarle el procesamiento a la siguiente ruta que coincida con
|
|
la petición usando <tt>pass</tt>:
|
|
|
|
get '/adivina/:quien' do
|
|
pass unless params[:quien] == 'Franco'
|
|
'Adivinaste!'
|
|
end
|
|
|
|
get '/adivina/*' do
|
|
'Erraste!'
|
|
end
|
|
|
|
Se sale inmediatamente del bloque de la ruta y se le pasa el control a la
|
|
siguiente ruta que coincida. Si no coincide ninguna ruta, se devuelve un 404.
|
|
|
|
=== Ejecutando Otra Ruta
|
|
|
|
Cuando querés obtener el resultado de la llamada a una ruta, +pass+ no te va a
|
|
servir. Para lograr esto, podés usar +call+:
|
|
|
|
get '/foo' do
|
|
status, headers, body = call env.merge("PATH_INFO" => '/bar')
|
|
[status, headers, body.map(&:upcase)]
|
|
end
|
|
|
|
get '/bar' do
|
|
"bar"
|
|
end
|
|
|
|
Notá que en el ejemplo anterior, es conveniente mover <tt>"bar"</tt> a un
|
|
helper, y llamarlo desde <tt>/foo</tt> y <tt>/bar</tt>. Así, vas a simplificar
|
|
las pruebas y a mejorar el rendimiento.
|
|
|
|
Si querés que la petición se envíe a la misma instancia de la aplicación en
|
|
lugar de a otra, usá <tt>call!</tt> en lugar de <tt>call</tt>.
|
|
|
|
En la especificación de Rack podés encontrar más información sobre
|
|
<tt>call</tt>.
|
|
|
|
=== Asignando el Código de Estado, los Encabezados y el Cuerpo de una Respuesta
|
|
|
|
Es posible, y se recomienda, asignar el código de estado y el cuerpo de una
|
|
respuesta con el valor de retorno de una ruta. De cualquier manera, en varios
|
|
escenarios, puede que sea conveniente asignar el cuerpo en un punto arbitrario
|
|
del flujo de ejecución con el método +body+. A partir de ahí, podés usar ese
|
|
mismo método para acceder al cuerpo de la respuesta:
|
|
|
|
get '/foo' do
|
|
body "bar"
|
|
end
|
|
|
|
after do
|
|
puts body
|
|
end
|
|
|
|
También es posible pasarle un bloque a +body+, que será ejecutado por el Rack
|
|
handler (podés usar esto para implementar streaming, mirá "Valores de retorno").
|
|
|
|
De manera similar, también podés asignar el código de estado y encabezados:
|
|
|
|
get '/foo' do
|
|
status 418
|
|
headers \
|
|
"Allow" => "BREW, POST, GET, PROPFIND, WHEN"
|
|
"Refresh" => "Refresh: 20; http://www.ietf.org/rfc/rfc2324.txt"
|
|
body "I'm a tea pot!"
|
|
end
|
|
|
|
También, al igual que +body+, tanto +status+ como +headers+ pueden utilizarse
|
|
para obtener sus valores cuando no se les pasa argumentos.
|
|
|
|
=== Log (Registro)
|
|
|
|
En el ámbito de la petición, el helper +logger+ (registrador) expone
|
|
una instancia de +Logger+:
|
|
|
|
get '/' do
|
|
logger.info "cargando datos"
|
|
# ...
|
|
end
|
|
|
|
Este logger tiene en cuenta la configuración de logueo de tu Rack
|
|
handler. Si el logueo está desactivado, este método va a devolver un
|
|
objeto que se comporta como un logger pero que en realidad no hace
|
|
nada. Así, no vas a tener que preocuparte por esta situación.
|
|
|
|
Tené en cuenta que el logueo está habilitado por defecto únicamente
|
|
para <tt>Sinatra::Application</tt>. Si heredaste de
|
|
<tt>Sinatra::Base</tt>, probablemente quieras habilitarlo manualmente:
|
|
|
|
class MiApp < Sinatra::Base
|
|
configure(:production, :development) do
|
|
enable :logging
|
|
end
|
|
end
|
|
|
|
=== Tipos Mime
|
|
|
|
Cuando usás <tt>send_file</tt> o archivos estáticos tal vez tengas tipos mime
|
|
que Sinatra no entiende. Usá +mime_type+ para registrarlos a través de la
|
|
extensión de archivo:
|
|
|
|
mime_type :foo, 'text/foo'
|
|
|
|
También lo podés usar con el ayudante +content_type+:
|
|
|
|
get '/' do
|
|
content_type :foo
|
|
"foo foo foo"
|
|
end
|
|
|
|
=== Generando URLs
|
|
|
|
Para generar URLs deberías usar el método +url+. Por ejemplo, en Haml:
|
|
|
|
%a{:href => url('/foo')} foo
|
|
|
|
Tiene en cuenta proxies inversos y encaminadores de Rack, si están presentes.
|
|
|
|
Este método también puede invocarse mediante su alias +to+ (mirá un ejemplo
|
|
a continuación).
|
|
|
|
=== Redirección del Navegador
|
|
|
|
Podés redireccionar al navegador con el método +redirect+:
|
|
|
|
get '/foo' do
|
|
redirect to('/bar')
|
|
end
|
|
|
|
Cualquier parámetro adicional se utiliza de la misma manera que los argumentos
|
|
pasados a +halt+:
|
|
|
|
redirect to('/bar'), 303
|
|
redirect 'http://google.com', 'te confundiste de lugar, compañero'
|
|
|
|
También podés redireccionar fácilmente de vuelta hacia la página desde donde
|
|
vino el usuario con +redirect back+:
|
|
|
|
get '/foo' do
|
|
"<a href='/bar'>hacer algo</a>"
|
|
end
|
|
|
|
get '/bar' do
|
|
hacer_algo
|
|
redirect back
|
|
end
|
|
|
|
Para pasar argumentos con una redirección, podés agregarlos a la cadena de
|
|
búsqueda:
|
|
|
|
redirect to('/bar?suma=42')
|
|
|
|
O usar una sesión:
|
|
|
|
enable :session
|
|
|
|
get '/foo' do
|
|
session[:secreto] = 'foo'
|
|
redirect to('/bar')
|
|
end
|
|
|
|
get '/bar' do
|
|
session[:secreto]
|
|
end
|
|
|
|
=== Cache Control
|
|
|
|
Asignar tus encabezados correctamente es el cimiento para realizar un cacheo
|
|
HTTP correcto.
|
|
|
|
Podés asignar el encabezado Cache-Control fácilmente:
|
|
|
|
get '/' do
|
|
cache_control :public
|
|
"cachealo!"
|
|
end
|
|
|
|
Pro tip: configurar el cacheo en un filtro +before+:
|
|
|
|
before do
|
|
cache_control :public, :must_revalidate, :max_age => 60
|
|
end
|
|
|
|
Si estás usando el helper +expires+ para definir el encabezado correspondiente,
|
|
<tt>Cache-Control</tt> se va a definir automáticamente:
|
|
|
|
before do
|
|
expires 500, :public, :must_revalidate
|
|
end
|
|
|
|
Para usar cachés adecuadamente, deberías considerar usar +etag+ y
|
|
+last_modified+. Es recomendable que llames a estos helpers *antes* de hacer
|
|
cualquier trabajo pesado, ya que van a enviar la respuesta inmediatamente si
|
|
el cliente ya tiene la versión actual en su caché:
|
|
|
|
get '/articulo/:id' do
|
|
@articulo = Articulo.find params[:id]
|
|
last_modified @articulo.updated_at
|
|
etag @articulo.sha1
|
|
erb :articulo
|
|
end
|
|
|
|
También es posible usar una
|
|
{weak ETag}[http://en.wikipedia.org/wiki/HTTP_ETag#Strong_and_weak_validation]:
|
|
|
|
etag @articulo.sha1, :weak
|
|
|
|
Estos helpers no van a cachear nada por vos, sino que van a facilitar la
|
|
información necesaria para poder hacerlo. Si estás buscando soluciones rápidas
|
|
de cacheo, mirá {rack-cache}[http://rtomayko.github.com/rack-cache/]:
|
|
|
|
require "rack/cache"
|
|
require "sinatra"
|
|
|
|
use Rack::Cache
|
|
|
|
get '/' do
|
|
cache_control :public, :max_age => 36000
|
|
sleep 5
|
|
"hola"
|
|
end
|
|
|
|
=== Enviando Archivos
|
|
|
|
Para enviar archivos, podés usar el método <tt>send_file</tt>:
|
|
|
|
get '/' do
|
|
send_file 'foo.png'
|
|
end
|
|
|
|
Además acepta un par de opciones:
|
|
|
|
send_file 'foo.png', :type => :jpg
|
|
|
|
Estas opciones son:
|
|
|
|
[filename]
|
|
nombre del archivo respondido, por defecto es el nombre real del archivo.
|
|
|
|
[last_modified]
|
|
valor para el encabezado Last-Modified, por defecto toma el mtime del archivo.
|
|
|
|
[type]
|
|
el content type que se va a utilizar, si no está presente se intenta adivinar
|
|
a partir de la extensión del archivo.
|
|
|
|
[disposition]
|
|
se utiliza para el encabezado Content-Disposition, y puede tomar alguno de los
|
|
siguientes valores: +nil+ (por defecto), <tt>:attachment</tt> e
|
|
<tt>:inline</tt>
|
|
|
|
[length]
|
|
encabezado Content-Length, por defecto toma el tamaño del archivo.
|
|
|
|
Si el Rack handler lo soporta, se intentará no transmitir directamente desde el
|
|
proceso de Ruby. Si usás este método, Sinatra se va a encargar automáticamente
|
|
peticiones de rango.
|
|
|
|
=== Accediendo al objeto de la petición
|
|
|
|
El objeto de la petición entrante puede ser accedido desde el nivel de la
|
|
petición (filtros, rutas y manejadores de errores) a través del método
|
|
<tt>request</tt>:
|
|
|
|
# app corriendo en http://ejemplo.com/ejemplo
|
|
get '/foo' do
|
|
t = %w[text/css text/html application/javascript]
|
|
request.accept # ['text/html', '*/*']
|
|
request.accept? 'text/xml' # true
|
|
request.preferred_type(t) # 'text/html'
|
|
request.body # cuerpo de la petición enviado por el cliente (ver más abajo)
|
|
request.scheme # "http"
|
|
request.script_name # "/ejemplo"
|
|
request.path_info # "/foo"
|
|
request.port # 80
|
|
request.request_method # "GET"
|
|
request.query_string # ""
|
|
request.content_length # longitud de request.body
|
|
request.media_type # tipo de medio de request.body
|
|
request.host # "ejemplo.com"
|
|
request.get? # true (hay métodos análogos para los otros verbos)
|
|
request.form_data? # false
|
|
request["UNA_CABECERA"] # valor de la cabecera UNA_CABECERA
|
|
request.referrer # la referencia del cliente o '/'
|
|
request.user_agent # user agent (usado por la condición :agent)
|
|
request.cookies # hash de las cookies del browser
|
|
request.xhr? # es una petición ajax?
|
|
request.url # "http://ejemplo.com/ejemplo/foo"
|
|
request.path # "/ejemplo/foo"
|
|
request.ip # dirección IP del cliente
|
|
request.secure? # false (sería true sobre ssl)
|
|
request.forwarded? # true (si se está corriendo atrás de un proxy inverso)
|
|
requuest.env # hash de entorno directamente entregado por Rack
|
|
end
|
|
|
|
Algunas opciones, como <tt>script_name</tt> o <tt>path_info</tt> pueden
|
|
también ser escritas:
|
|
|
|
before { request.path_info = "/" }
|
|
|
|
get "/" do
|
|
"todas las peticiones llegan acá"
|
|
end
|
|
|
|
El objeto <tt>request.body</tt> es una instancia de IO o StringIO:
|
|
|
|
post "/api" do
|
|
request.body.rewind # en caso de que alguien ya lo haya leído
|
|
datos = JSON.parse request.body.read
|
|
"Hola #{datos['nombre']}!"
|
|
end
|
|
|
|
=== Archivos Adjuntos
|
|
|
|
Podés usar el método helper +attachment+ para indicarle al navegador que
|
|
almacene la respuesta en el disco en lugar de mostrarla en pantalla:
|
|
|
|
get '/' do
|
|
attachment
|
|
"guardalo!"
|
|
end
|
|
|
|
También podés pasarle un nombre de archivo:
|
|
|
|
get '/' do
|
|
attachment "info.txt"
|
|
"guardalo!"
|
|
end
|
|
|
|
=== Buscando los Archivos de las Plantillas
|
|
|
|
El helper <tt>find_template</tt> se utiliza para encontrar los archivos de las
|
|
plantillas que se van a renderizar:
|
|
|
|
find_template settings.views, 'foo', Tilt[:haml] do |archivo|
|
|
puts "podría ser #{archivo}"
|
|
end
|
|
|
|
Si bien esto no es muy útil, lo interesante es que podés sobreescribir este
|
|
método, y así enganchar tu propio mecanismo de búsqueda. Por ejemplo, para
|
|
poder utilizar más de un directorio de vistas:
|
|
|
|
set :views, ['vistas', 'plantillas']
|
|
|
|
helpers do
|
|
def find_template(views, name, engine, &block)
|
|
Array(views).each { |v| super(v, name, engine, &block) }
|
|
end
|
|
end
|
|
|
|
Otro ejemplo consiste en usar directorios diferentes para los distintos motores
|
|
de renderizado:
|
|
|
|
set :views, :sass => 'vistas/sass', :haml => 'plantillas', :defecto => 'vistas'
|
|
|
|
helpers do
|
|
def find_template(views, name, engine, &block)
|
|
_, folder = views.detect { |k,v| engine == Tilt[k] }
|
|
folder ||= views[:defecto]
|
|
super(folder, name, engine, &block)
|
|
end
|
|
end
|
|
|
|
¡Es muy fácil convertir estos ejemplos en una extensión y compartirla!.
|
|
|
|
Notá que <tt>find_template</tt> no verifica si un archivo existe realmente, sino
|
|
que llama al bloque que recibe para cada path posible. Esto no representa un
|
|
problema de rendimiento debido a que +render+ va a usar +break+ ni bien
|
|
encuentre un archivo que exista. Además, las ubicaciones de las plantillas (y
|
|
su contenido) se cachean cuando no estás en el modo de desarrollo. Es bueno
|
|
tener en cuenta lo anteiror si escribís un método medio loco.
|
|
|
|
== Configuración
|
|
|
|
Ejecutar una vez, en el inicio, en cualquier entorno:
|
|
|
|
configure do
|
|
# asignando una opción
|
|
set :opcion, 'valor'
|
|
|
|
# asignando varias opciones
|
|
set :a => 1, :b => 2
|
|
|
|
# atajo para `set :opcion, true`
|
|
enable :opcion
|
|
|
|
# atajo para `set :opcion, false`
|
|
disable :opcion
|
|
|
|
# también podés tener configuraciones dinámicas usando bloques
|
|
set(:css_dir) { File.join(views, 'css') }
|
|
end
|
|
|
|
Ejecutar únicamente cuando el entorno (la variable de entorno RACK_ENV) es
|
|
<tt>:production</tt>:
|
|
|
|
configure :production do
|
|
...
|
|
end
|
|
|
|
Ejecutar cuando el entorno es <tt>:production</tt> o <tt>:test</tt>:
|
|
|
|
configure :production, :test do
|
|
...
|
|
end
|
|
|
|
Podés acceder a estas opciones utilizando el método <tt>settings</tt>:
|
|
|
|
configure do
|
|
set :foo, 'bar'
|
|
end
|
|
|
|
get '/' do
|
|
settings.foo? # => true
|
|
settings.foo # => 'bar'
|
|
...
|
|
end
|
|
|
|
=== Configuraciones Disponibles
|
|
|
|
[absolute_redirects] si está deshabilitada, Sinatra va a permitir redirecciones
|
|
relativas, sin embargo, como consecuencia de esto, va a
|
|
dejar de cumplir con el RFC 2616 (HTTP 1.1), que solamente
|
|
permite redirecciones absolutas.
|
|
|
|
Activalo si tu apliación está corriendo atrás de un proxy
|
|
inverso que no se ha configurado adecuadamente. Notá que
|
|
el helper +url+ va a seguir produciendo URLs absolutas, a
|
|
menos que le pasés +false+ como segundo parámetro.
|
|
|
|
Deshabilitada por defecto.
|
|
|
|
[add_charsets] tipos mime a los que el helper <tt>content_type</tt> les
|
|
añade automáticamente el charset.
|
|
|
|
En general, no deberías asignar directamente esta opción,
|
|
sino añadirle los charsets que quieras:
|
|
|
|
settings.add_charsets << "application/foobar"
|
|
|
|
[app_file] archivo principal de la aplicación, se utiliza para
|
|
detectar la raíz del proyecto, el directorio de las vistas
|
|
y el público así como las plantillas inline.
|
|
|
|
[bind] dirección IP que utilizará el servidor integrado (por
|
|
defecto: 0.0.0.0).
|
|
|
|
[default_encoding] encoding utilizado cuando el mismo se desconoce (por
|
|
defecto <tt>"utf-8"</tt>).
|
|
|
|
[dump_errors] mostrar errores en el log.
|
|
|
|
[environment] entorno actual, por defecto toma el valor de
|
|
<tt>ENV['RACK_ENV']</tt>, o <tt>"development"</tt> si no
|
|
está disponible.
|
|
|
|
[logging] define si se utiliza el logger.
|
|
|
|
[lock] coloca un lock alrededor de cada petición, procesando
|
|
solamente una por proceso.
|
|
|
|
Habilitá esta opción si tu aplicación no es thread-safe.
|
|
Se encuentra deshabilitada por defecto.
|
|
|
|
[method_override] utiliza el parámetro <tt>_method</tt> para permtir
|
|
formularios put/delete en navegadores que no los soportan.
|
|
|
|
[port] puerto en el que escuchará el servidor integrado.
|
|
|
|
[prefixed_redirects] define si inserta <tt>request.script_name</tt> en las
|
|
redirecciones cuando no se proporciona un path absoluto.
|
|
De esta manera, cuando está habilitada,
|
|
<tt>redirect '/foo'</tt> se comporta de la misma manera
|
|
que <tt>redirect to('/foo')</tt>. Se encuentra
|
|
deshabilitada por defecto.
|
|
|
|
[public] directorio desde donde se sirven los archivos públicos.
|
|
|
|
[reload_templates] define si se recargan las plantillas entre peticiones.
|
|
|
|
Se encuentra activado en el entorno de desarrollo.
|
|
|
|
[root] directorio raíz del proyecto.
|
|
|
|
[raise_errors] elevar excepciones (detiene la aplicación).
|
|
|
|
[run] cuando está habilitada, Sinatra se va a encargar de
|
|
iniciar el servidor web, no la habilités cuando estés
|
|
usando rackup o algún otro medio.
|
|
|
|
[running] indica si el servidor integrado está ejecutandose, ¡no
|
|
cambiés esta configuración!.
|
|
|
|
[server] servidor, o lista de servidores, para usar como servidor
|
|
integrado. Por defecto: ['thin', 'mongrel', 'webrick'],
|
|
el orden establece la prioridad.
|
|
|
|
[sessions] habilita sesiones basadas en cookies.
|
|
|
|
[show_exceptions] muestra un stack trace en el navegador.
|
|
|
|
[static] define si Sinatra debe encargarse de servir archivos
|
|
estáticos.
|
|
|
|
Deshabilitala cuando usés un servidor capaz de
|
|
hacerlo por sí solo, porque mejorará el
|
|
rendimiento. Se encuentra habilitada por
|
|
defecto en el estilo clásico y desactivado en el
|
|
el modular.
|
|
|
|
[views] directorio de las vistas.
|
|
|
|
== Manejo de Errores
|
|
|
|
Los manejadores de errores se ejecutan dentro del mismo contexto que las rutas
|
|
y los filtros +before+, lo que significa que podés usar, por ejemplo,
|
|
<tt>haml</tt>, <tt>erb</tt>, <tt>halt</tt>, etc.
|
|
|
|
=== No encontrado <em>(Not Found)</em>
|
|
|
|
Cuando se eleva una excepción <tt>Sinatra::NotFound</tt>, o el código de
|
|
estado de la respuesta es 404, el manejador <tt>not_found</tt> es invocado:
|
|
|
|
not_found do
|
|
'No existo'
|
|
end
|
|
|
|
=== Error
|
|
|
|
El manejador +error+ es invocado cada vez que una excepción es elevada
|
|
desde un bloque de ruta o un filtro. El objeto de la excepción se puede
|
|
obtener de la variable Rack <tt>sinatra.error</tt>:
|
|
|
|
error do
|
|
'Disculpá, ocurrió un error horrible - ' + env['sinatra.error'].name
|
|
end
|
|
|
|
Errores personalizados:
|
|
|
|
error MiErrorPersonalizado do
|
|
'Lo que pasó fue...' + env['sinatra.error'].message
|
|
end
|
|
|
|
Entonces, si pasa esto:
|
|
|
|
get '/' do
|
|
raise MiErrorPersonalizado, 'algo malo'
|
|
end
|
|
|
|
Obtenés esto:
|
|
|
|
Lo que pasó fue... algo malo
|
|
|
|
También, podés instalar un manejador de errores para un código de estado:
|
|
|
|
error 403 do
|
|
'Acceso prohibido'
|
|
end
|
|
|
|
get '/secreto' do
|
|
403
|
|
end
|
|
|
|
O un rango:
|
|
|
|
error 400..510 do
|
|
'Boom'
|
|
end
|
|
|
|
Sinatra instala manejadores <tt>not_found</tt> y <tt>error</ttt> especiales
|
|
cuando se ejecuta dentro del entorno de desarrollo "development".
|
|
|
|
== Rack Middleware
|
|
|
|
Sinatra corre sobre Rack[http://rack.rubyforge.org/], una interfaz minimalista
|
|
que es un estándar para frameworks webs escritos en Ruby. Una de las
|
|
capacidades más interesantes de Rack para los desarrolladores de aplicaciones
|
|
es el soporte de "middleware" -- componentes que se ubican entre el servidor y
|
|
tu aplicación, supervisando y/o manipulando la petición/respuesta HTTP para
|
|
proporcionar varios tipos de funcionalidades comunes.
|
|
|
|
Sinatra hace muy sencillo construir tuberías de Rack middleware a través del
|
|
método top-level +use+:
|
|
|
|
require 'sinatra'
|
|
require 'mi_middleware_personalizado'
|
|
|
|
use Rack::Lint
|
|
use MiMiddlewarePersonalizado
|
|
|
|
get '/hola' do
|
|
'Hola Mundo'
|
|
end
|
|
|
|
Las semánticas de +use+ son idénticas a las definidas para el DSL
|
|
Rack::Builder[http://rack.rubyforge.org/doc/classes/Rack/Builder.html] (más
|
|
frecuentemente usado desde archivos rackup). Por ejemplo, el método +use+
|
|
acepta argumentos múltiples/variables así como bloques:
|
|
|
|
use Rack::Auth::Basic do |nombre_de_usuario, password|
|
|
nombre_de_usuario == 'admin' && password == 'secreto'
|
|
end
|
|
|
|
Rack es distribuido con una variedad de middleware estándar para logging,
|
|
debugging, enrutamiento URL, autenticación, y manejo de sesiones. Sinatra
|
|
usa muchos de estos componentes automáticamente de acuerdo a su configuración
|
|
para que típicamente no tengas que usarlas (con +use+) explícitamente.
|
|
|
|
== Pruebas
|
|
|
|
Las pruebas para las aplicaciones Sinatra pueden ser escritas utilizando
|
|
cualquier framework o librería de pruebas basada en Rack. Se recomienda usar
|
|
{Rack::Test}[http://gitrdoc.com/brynary/rack-test]:
|
|
|
|
require 'mi_app_sinatra'
|
|
require 'test/unit'
|
|
require 'rack/test'
|
|
|
|
class MiAppTest < Test::Unit::TestCase
|
|
include Rack::Test::Methods
|
|
|
|
def app
|
|
Sinatra::Application
|
|
end
|
|
|
|
def test_mi_defecto
|
|
get '/'
|
|
assert_equal 'Hola Mundo!', last_response.body
|
|
end
|
|
|
|
def test_con_parametros
|
|
get '/saludar', :name => 'Franco'
|
|
assert_equal 'Hola Frank!', last_response.body
|
|
end
|
|
|
|
def test_con_entorno_rack
|
|
get '/', {}, 'HTTP_USER_AGENT' => 'Songbird'
|
|
assert_equal "Estás usando Songbird!", last_response.body
|
|
end
|
|
end
|
|
|
|
NOTA: El módulo Sinatra::Test y la clase Sinatra::TestHarness están
|
|
deprecados a partir de la versión 0.9.2.
|
|
|
|
== Sinatra::Base - Middleware, Librerías, y Aplicaciones Modulares
|
|
|
|
Definir tu aplicación en el top-level funciona bien para micro-aplicaciones
|
|
pero trae inconvenientes considerables a la hora de construir componentes
|
|
reutilizables como Rack middleware, Rails metal, simple librerías con un
|
|
componente de servidor, o incluso extensiones de Sinatra. El DSL de top-level
|
|
contamina el espacio de nombres de Object y asume una configuración apropiada
|
|
para micro-aplicaciones (por ejemplo, un único archivo de aplicación, los
|
|
directorios ./public y ./views, logging, página con detalles de excepción,
|
|
etc.). Ahí es donde Sinatra::Base entra en el juego:
|
|
|
|
require 'sinatra/base'
|
|
|
|
class MiApp < Sinatra::Base
|
|
set :sessions, true
|
|
set :foo, 'bar'
|
|
|
|
get '/' do
|
|
'Hola Mundo!'
|
|
end
|
|
end
|
|
|
|
Las subclases de Sinatra::Base tienen disponibles exactamente los mismos
|
|
métodos que los provistos por el DSL de top-level. La mayoría de las
|
|
aplicaciones top-level se pueden convertir en componentes Sinatra::Base con
|
|
dos modificaciones:
|
|
|
|
* Tu archivo debe requerir <tt>sinatra/base</tt> en lugar de +sinatra+; de otra
|
|
manera, todos los métodos del DSL de sinatra son importados dentro del
|
|
espacio de nombres principal.
|
|
* Poné las rutas, manejadores de errores, filtros y opciones de tu aplicación
|
|
en una subclase de Sinatra::Base.
|
|
|
|
<tt>Sinatra::Base</tt> es una pizarra en blanco. La mayoría de las opciones están
|
|
desactivadas por defecto, incluyendo el servidor incorporado. Mirá
|
|
{Opciones y Configuraciones}[http://sinatra.github.com/configuration.html]
|
|
para detalles sobre las opciones disponibles y su comportamiento.
|
|
|
|
=== Estilo Modular vs. Clásico
|
|
|
|
Contrariamente a la creencia popular, no hay nada de malo con el estilo clásico.
|
|
Si se ajusta a tu aplicación, no es necesario que la cambies a una modular.
|
|
|
|
Existen tan solo dos desventajas en comparación con el estilo modular:
|
|
|
|
* Solamente podés tener una aplicación Sinatra por proceso Ruby - si tenés
|
|
planificado usar más, cambiá al estilo modular.
|
|
|
|
* El estilo clásico contamina Object con métodos delegadores - si tenés
|
|
planificado empaquetar tu aplicación en una librería/gem, cambiá al estilo
|
|
modular.
|
|
|
|
No hay ninguna razón por la cuál no puedas mezclar los estilos modular y
|
|
clásico.
|
|
|
|
Cuando cambiés de un estilo al otro, tené en cuenta las sutiles diferencias
|
|
entre sus configuraciones:
|
|
|
|
Configuración Clásica Modular
|
|
|
|
app_file archivo que carga sinatra nil
|
|
run $0 == app_file false
|
|
logging true false
|
|
method_override true false
|
|
inline_templates true false
|
|
static true false
|
|
|
|
=== Sirviendo una Aplicación Modular
|
|
|
|
Las dos opciones más comunes para iniciar una aplicación modular son, iniciarla
|
|
activamente con <tt>run!</tt>:
|
|
|
|
# mi_app.rb
|
|
require 'sinatra/base'
|
|
|
|
class MiApp < Sinatra::Base
|
|
# ... código de la app ...
|
|
|
|
# iniciar el servidor si el archivo fue ejecutado directamente
|
|
run! if app_file == $0
|
|
end
|
|
|
|
Iniciar con:
|
|
|
|
ruby mi_app.rb
|
|
|
|
O, con un archivo <tt>config.ru</tt>, que permite usar cualquier handler Rack:
|
|
|
|
# config.ru
|
|
require 'mi_app'
|
|
run MiApp
|
|
|
|
Después ejecutar:
|
|
|
|
rackup -p 4567
|
|
|
|
=== Usando una Aplicación Clásica con un Archivo config.ru
|
|
|
|
Escribí el archivo de tu aplicación:
|
|
|
|
# app.rb
|
|
require 'sinatra'
|
|
|
|
get '/' do
|
|
'Hola mundo!'
|
|
end
|
|
|
|
Y el <tt>config.ru</tt> correspondiente:
|
|
|
|
require 'app'
|
|
run Sinatra::Application
|
|
|
|
=== ¿Cuándo Usar config.ru?
|
|
|
|
Indicadores de que probablemente querés usar <tt>config.ru</tt>:
|
|
|
|
* Querés realizar el deploy con un hanlder Rack distinto (Passenger, Unicorn,
|
|
Heroku, ...).
|
|
* Querés usar más de una subclase de <tt>Sinatra::Base</tt>.
|
|
* Querés usar Sinatra únicamente para middleware, pero no como un endpoint.
|
|
|
|
<b>No hay necesidad de utilizar un archivo <tt>config.ru</tt> exclusivamente
|
|
porque tenés una aplicación modular, y no necesitás una aplicación modular para
|
|
iniciarla con <tt>config.ru</tt>.</b>
|
|
|
|
=== Utilizando Sinatra como Middleware
|
|
|
|
Sinatra no solo es capaz de usar otro Rack middleware, sino que a su vez,
|
|
cualquier aplicación Sinatra puede ser agregada delante de un endpoint Rack
|
|
como middleware. Este endpoint puede ser otra aplicación Sinatra, o cualquier
|
|
aplicación basada en Rack (Rails/Ramaze/Camping/...):
|
|
|
|
require 'sinatra/base'
|
|
|
|
class PantallaDeLogin < Sinatra::Base
|
|
enable :sessions
|
|
|
|
get('/login') { haml :login }
|
|
|
|
post('/login') do
|
|
if params[:nombre] = 'admin' and params[:password] = 'admin'
|
|
session['nombre_de_usuario'] = params[:nombre]
|
|
else
|
|
redirect '/login'
|
|
end
|
|
end
|
|
end
|
|
|
|
class MiApp < Sinatra::Base
|
|
# el middleware se ejecutará antes que los filtros
|
|
use PantallaDeLogin
|
|
|
|
before do
|
|
unless session['nombre_de_usuario']
|
|
halt "Acceso denegado, por favor <a href='/login'>iniciá sesión</a>."
|
|
end
|
|
end
|
|
|
|
get('/') { "Hola #{session['nombre_de_usuario']}." }
|
|
end
|
|
|
|
=== Creación Dinámica de Aplicaciones
|
|
|
|
Puede que en algunas ocasiones quieras crear nuevas aplicaciones en
|
|
tiempo de ejecución sin tener que asignarlas a una constante. Para
|
|
esto tenés `Sinatra.new`:
|
|
|
|
require 'sinatra/base'
|
|
mi_app = Sinatra.new { get('/') { "hola" } }
|
|
mi_app.run!
|
|
|
|
Acepta como argumento opcional una aplicación desde la que se
|
|
heredará:
|
|
|
|
require 'sinatra/base'
|
|
|
|
controller = Sinatra.new do
|
|
enable :logging
|
|
helpers MisHelpers
|
|
end
|
|
|
|
map('/a') do
|
|
run Sinatra.new(controller) { get('/') { 'a' } }
|
|
end
|
|
|
|
map('/b') do
|
|
run Sinatra.new(controller) { get('/') { 'b' } }
|
|
end
|
|
|
|
Construir aplicaciones de esta forma resulta especialmente útil para
|
|
testear extensiones Sinatra o para usar Sinatra en tus librerías.
|
|
|
|
Por otro lado, hace extremadamente sencillo usar Sinatra como
|
|
middleware:
|
|
|
|
require 'sinatra/base'
|
|
|
|
use Sinatra do
|
|
get('/') { ... }
|
|
end
|
|
|
|
run ProyectoRails::Application
|
|
|
|
== Ámbitos y Ligaduras
|
|
|
|
El ámbito en el que te encontrás determina que métodos y variables están
|
|
disponibles.
|
|
|
|
=== Ámbito de Aplicación/Clase
|
|
|
|
Cada aplicación Sinatra es una subclase de Sinatra::Base. Si estás usando el
|
|
DSL de top-level (<tt>require 'sinatra'</tt>), entonces esta clase es
|
|
Sinatra::Application, de otra manera es la subclase que creaste explícitamente.
|
|
Al nivel de la clase tenés métodos como +get+ o +before+, pero no podés acceder
|
|
a los objetos +request+ o +session+, ya que hay una única clase de la
|
|
aplicación para todas las peticiones.
|
|
|
|
Las opciones creadas utilizando +set+ son métodos al nivel de la clase:
|
|
|
|
class MiApp < Sinatra::Base
|
|
# Ey, estoy en el ámbito de la aplicación!
|
|
set :foo, 42
|
|
foo # => 42
|
|
|
|
get '/foo' do
|
|
# Hey, ya no estoy en el ámbito de la aplicación!
|
|
end
|
|
end
|
|
|
|
Tenés la ligadura al ámbito de la aplicación dentro de:
|
|
|
|
* El cuerpo de la clase de tu aplicación
|
|
* Métodos definidos por extensiones
|
|
* El bloque pasado a +helpers+
|
|
* Procs/bloques usados como el valor para +set+
|
|
|
|
Este ámbito puede alcanzarse de las siguientes maneras:
|
|
|
|
* A través del objeto pasado a los bloques de configuración (<tt>configure { |c| ...}</tt>)
|
|
* Llamando a +settings+ desde dentro del ámbito de la petición
|
|
|
|
=== Ámbito de Petición/Instancia
|
|
|
|
Para cada petición entrante, una nueva instancia de la clase de tu aplicación
|
|
es creada y todos los bloques de rutas son ejecutados en ese ámbito. Desde este
|
|
ámbito podés acceder a los objetos +request+ y +session+ o llamar a los métodos
|
|
de renderización como +erb+ o +haml+. Podés acceder al ámbito de la aplicación
|
|
desde el ámbito de la petición utilizando +settings+:
|
|
|
|
class MiApp < Sinatra::Base
|
|
# Ey, estoy en el ámbito de la aplicación!
|
|
get '/definir_ruta/:nombre' do
|
|
# Ámbito de petición para '/definir_ruta/:nombre'
|
|
@valor = 42
|
|
|
|
settings.get("/#{params[:nombre]}") do
|
|
# Ámbito de petición para "/#{params[:nombre]}"
|
|
@valor # => nil (no es la misma petición)
|
|
end
|
|
|
|
"Ruta definida!"
|
|
end
|
|
end
|
|
|
|
Tenés la ligadura al ámbito de la petición dentro de:
|
|
|
|
* bloques pasados a get/head/post/put/delete/options
|
|
* filtros before/after
|
|
* métodos ayudantes
|
|
* plantillas/vistas
|
|
|
|
=== Ámbito de Delegación
|
|
|
|
El ámbito de delegación solo reenvía métodos al ámbito de clase. De cualquier
|
|
manera, no se comporta 100% como el ámbito de clase porque no tenés la ligadura
|
|
de la clase: únicamente métodos marcados explícitamente para delegación están
|
|
disponibles y no compartís variables/estado con el ámbito de clase (léase:
|
|
tenés un +self+ diferente). Podés agregar delegaciones de método llamando a
|
|
<tt>Sinatra::Delegator.delegate :nombre_del_metodo</tt>.
|
|
|
|
Tenés la ligadura al ámbito de delegación dentro de:
|
|
|
|
* La ligadura del top-level, si hiciste <tt>require "sinatra"</tt>
|
|
* Un objeto extendido con el mixin <tt>Sinatra::Delegator</tt>
|
|
|
|
Pegale una mirada al código: acá está el
|
|
{Sinatra::Delegator mixin}[http://github.com/sinatra/sinatra/blob/ceac46f0bc129a6e994a06100aa854f606fe5992/lib/sinatra/base.rb#L1128]
|
|
que es {incluido en el espacio de nombres principal}[http://github.com/sinatra/sinatra/blob/ceac46f0bc129a6e994a06100aa854f606fe5992/lib/sinatra/main.rb#L28].
|
|
|
|
== Línea de Comandos
|
|
|
|
Las aplicaciones Sinatra pueden ser ejecutadas directamente:
|
|
|
|
ruby miapp.rb [-h] [-x] [-e ENTORNO] [-p PUERTO] [-o HOST] [-s MANEJADOR]
|
|
|
|
Las opciones son:
|
|
|
|
-h # ayuda
|
|
-p # asigna el puerto (4567 es usado por defecto)
|
|
-o # asigna el host (0.0.0.0 es usado por defecto)
|
|
-e # asigna el entorno (development es usado por defecto)
|
|
-s # especifica el servidor/manejador rack (thin es usado por defecto)
|
|
-x # activa el mutex lock (está desactivado por defecto)
|
|
|
|
== Versiones de Ruby Soportadas
|
|
|
|
Las siguientes versiones de Ruby son soportadas oficialmente:
|
|
|
|
[ Ruby 1.8.7 ]
|
|
1.8.7 es soportado completamente. Sin embargo, si no hay nada que te lo
|
|
prohíba, te recomendamos que usés 1.9.2 o cambies a JRuby o Rubinius.
|
|
|
|
[ Ruby 1.9.2 ]
|
|
1.9.2 es soportado y recomendado. Tené en cuenta que Radius y Markaby no
|
|
son compatibles con 1.9 actualmente. Además, no usés 1.9.2p0, porque produce
|
|
fallos de segmentación cuando se utiliza Sinatra.
|
|
|
|
[ Rubinius ]
|
|
Rubinius es soportado oficialmente (Rubinius >= 1.2.3). Todo
|
|
funciona correctamente, incluyendo los lenguajes de plantillas.
|
|
|
|
[ JRuby ]
|
|
JRuby es soportado oficialmente (JRuby >= 1.6.0). No se conocen
|
|
problemas con librerías de plantillas de terceras partes. Sin
|
|
embargo, si elegís usar JRuby, deberías examinar sus Rack handlers
|
|
porque el servidor web Thin no es soportado completamente. El
|
|
soporte de JRuby para extensiones C se encuentra en una etapa
|
|
experimental, sin embargo, de momento solo RDiscount se ve afectada.
|
|
|
|
<b>Hemos dejado de soportar Ruby 1.8.6.</b>
|
|
|
|
Siempre le prestamos atención a las nuevas versiones de Ruby.
|
|
|
|
Las siguientes implementaciones de Ruby no se encuentran soportadas
|
|
oficialmente. De cualquier manera, pueden ejecutar Sinatra:
|
|
|
|
* Versiones anteriores de JRuby y Rubinius
|
|
* MacRuby, Maglev e IronRuby
|
|
* Ruby 1.9.0 y 1.9.1
|
|
* Ruby 1.8.6 con {backports}[https://github.com/marcandre/backports/#readme]
|
|
|
|
No estar soportada oficialmente, significa que si las cosas solamente se rompen
|
|
ahí y no en una plataforma soportada, asumimos que no es nuestro problema sino
|
|
el suyo.
|
|
|
|
Nuestro servidor CI también se ejecuta sobre ruby-head (que será la
|
|
próxima versión 1.9.3). Como está en movimiento constante, no podemos
|
|
garantizar nada. De todas formas, podés contar con que 1.9.3-p0 sea
|
|
soportada.
|
|
|
|
Sinatra debería funcionar en cualquier sistema operativo soportado por la
|
|
implementación de Ruby elegida.
|
|
|
|
== A la Vanguardia
|
|
|
|
Si querés usar el código de Sinatra más reciente, sentite libre de ejecutar
|
|
tu aplicación sobre la rama master, en general es bastante estable.
|
|
|
|
También liberamos prereleases de vez en cuando, así, podés hacer
|
|
|
|
gem install sinatra --pre
|
|
|
|
Para obtener algunas de las últimas características.
|
|
|
|
=== Con Bundler
|
|
|
|
Esta es la manera recomendada para ejecutar tu aplicación sobre la última
|
|
versión de Sinatra usando {Bundler}[http://gembundler.com/].
|
|
|
|
Primero, instalá bundler si no lo hiciste todavía:
|
|
|
|
gem install bundler
|
|
|
|
Después, en el directorio de tu proyecto, creá un archivo +Gemfile+:
|
|
|
|
source :rubygems
|
|
gem 'sinatra', :git => "git://github.com/sinatra/sinatra.git"
|
|
|
|
# otras dependencias
|
|
gem 'haml' # por ejemplo, si usás haml
|
|
gem 'activerecord', '~> 3.0' # quizás también necesités ActiveRecord 3.x
|
|
|
|
Tené en cuenta que tenés que listar todas las dependencias directas de tu
|
|
aplicación. No es necesario listar las dependencias de Sinatra (Rack y Tilt)
|
|
porque Bundler las agrega directamente.
|
|
|
|
Ahora podés arrancar tu aplicación así:
|
|
|
|
bundle exec ruby miapp.rb
|
|
|
|
=== Con Git
|
|
|
|
Cloná el repositorio localmente y ejecutá tu aplicación, asegurándote que el
|
|
directorio <tt>sinatra/lib</tt> esté en el <tt>$LOAD_PATH</tt>:
|
|
|
|
cd miapp
|
|
git clone git://github.com/sinatra/sinatra.git
|
|
ruby -Isinatra/lib miapp.rb
|
|
|
|
Para actualizar el código fuente de Sinatra en el futuro:
|
|
|
|
cd miapp/sinatra
|
|
git pull
|
|
|
|
=== Instalación Global
|
|
|
|
Podés construir la gem vos mismo:
|
|
|
|
git clone git://github.com/sinatra/sinatra.git
|
|
cd sinatra
|
|
rake sinatra.gemspec
|
|
rake install
|
|
|
|
Si instalás tus gems como root, el último paso debería ser
|
|
|
|
sudo rake install
|
|
|
|
== Versionado
|
|
|
|
Sinatra utiliza el {Versionado Semántico}[http://semver.org/],
|
|
siguiendo las especificaciones SemVer y SemVerTag.
|
|
|
|
== Lecturas Recomendadas
|
|
|
|
* {Sito web del proyecto}[http://www.sinatrarb.com/] - Documentación
|
|
adicional, noticias, y enlaces a otros recursos.
|
|
* {Contribuyendo}[http://www.sinatrarb.com/contributing] - ¿Encontraste un
|
|
error?. ¿Necesitás ayuda?. ¿Tenés un parche?.
|
|
* {Seguimiento de problemas}[http://github.com/sinatra/sinatra/issues]
|
|
* {Twitter}[http://twitter.com/sinatra]
|
|
* {Lista de Correo}[http://groups.google.com/group/sinatrarb/topics]
|
|
* {IRC: #sinatra}[irc://chat.freenode.net/#sinatra] en http://freenode.net
|
|
* Documentación de la API para la
|
|
{última versión liberada}[http://rubydoc.info/gems/sinatra] o para la
|
|
{rama de desarrollo actual}[http://rubydoc.info/github/sinatra/sinatra]
|
|
en http://rubydoc.info/
|