Sinatra is really fun to work with. It's small and fast. It doesn't make many assumptions. If something goes wrong, it's pretty easy to go into the source and figure out what is going on.
There were a couple projects I wanted to take for a spin: Sinatra, Datamapper, HAML, and SASS. I decided to roll them all up into one proof-of-concept project. I don't go into a lot of depth on each, just enough to know that I can get it all up and running.
Also, since I'm running Passenger on some production boxes now, I wanted to deploy my Sinatra/Datamapper app through Passenger's Rack support.
Sinatra doesn't care how you organize your application. You can put everything in one file, or start to break things out into a Rails-ish directory structure. Of course, if you break things out into separate files, you'll need to require files as necessary -- Sinatra doesn't have conventions like Rails does about where to find things.
I put everything except for views in one file. So configurations, models, and actions all went in main.rb. My directory structure looks like this:
/app
main.rb
/views
index.haml
layout.haml
style.sass
/config
deploy.rb
/public
Capfile
Rakefile
config.ru
The /app and /views directories might be overkill for a simple app like this. Coming from Rails, it was easy to lay things out like this since it's instantly recognizable to me.
My proof-of-concept app is a simple to-do list. It has one model, Todo. Basically, I just needed a testing ground for a DataMapper model.
# main.rb
require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-validations'
require 'logger'
## CONFIGURATION
configure :development do
DataMapper.setup(:default, {
:adapter => 'mysql',
:host => 'localhost',
:username => 'root' ,
:password => '',
:database => 'sinatra_development'})
DataMapper::Logger.new(STDOUT, :debug)
end
configure :production do
DataMapper.setup(:default, {
:adapter => 'mysql',
:host => 'localhost',
:username => 'user' ,
:password => 'pass',
:database => 'sinatra_production'})
end
### MODELS
class Todo
include DataMapper::Resource
property :id, Integer, :serial=>true
property :title, String
property :created_at, DateTime
property :complete, Boolean, :default=>false
validates_present :title
end
### CONTROLLER ACTIONS
# index
get '/' do
@todos=Todo.all :order=>[:created_at]
haml :index
end
# create
post '/' do
todo=Todo.create(:title=>params[:title],:created_at=>Time.now)
redirect '/'
end
# mark complete / incomplete
get '/:id/mark/:is_complete' do
todo=Todo.get(params[:id])
todo.update_attributes(:complete=>(params[:is_complete]=='complete'))
redirect '/'
end
get '/:id/delete' do
todo=Todo.get(params[:id]).destroy
redirect '/'
end
# SASS stylesheet
get '/stylesheets/style.css' do
header 'Content-Type' => 'text/css; charset=utf-8'
sass :style
end
Notes on this: * for my purposes, it was enough to just log DataMapper to Standard out in development (that's the DataMapper::Logger.new(STDOUT, :debug) line). A more robust solution would be to figure out how use Rack's logging mechanism. * note that I passed symbols to my configure blocks. If you use symbols to represent your environment, make sure you set your environment to a symbol as well. See the config.ru file below.
require 'rubygems'
require 'sinatra.rb'
# Sinatra defines #set at the top level as a way to set application configuration
set :views, File.join(File.dirname(__FILE__), 'app','views')
set :run, false
set :env, (ENV['RACK_ENV'] ? ENV['RACK_ENV'].to_sym : :development)
require 'app/main'
run Sinatra.application
Some notes here: * Sinatra defines #set at the top level as a way to set application configuration. You'll see alternate ways to specify configuration, like providing multiple values in a hash, or even setting the Sinatra::defaultoptions hash directly. * the set :env converts ENV['RACKENV'] to a symbol -- that's so it will match the symbol passed to the configure block in main.rb
Within Rails, Rake includes everything in lib/tasks with a glob ... there's nothing hard and fast about this, it's just a Rails convention. I put my one task in Rakefile itself, but if you had more, you could easily decide where you wanted your blah.task files, and include them from Rakefile just like Rails does.
My one rake task rebuilt my schema from my DM model definition. Note this is destructive -- it blows away all the data in your table whenever you run it.
task :migrate do
DataMapper.auto_migrate!
end
I wanted to do a simple upload to my production server, with no SCM involved. It turns out Capistrano makes that easy. In deploy.rb, just do:
# simple upload -- no scm involved
set :repository, "."
set :scm, :none
set :deploy_via, :copy
Cap will zip everything up, scp to your server, and do its usual symlinking etc. Very nice for quick proof-of-concepts!
Like Rake, Capistrano for Rails is driven by "load" directives within Capfile. This is where the "Rails" conventions are bootstrapped:
Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } # this loads anything from your installed your plugins
load 'config/deploy' # this is your actual deploy.rb # it uses all that stuff in the gem's deploy.rb
#new-todo
%form{:method => "post"}
%input{:type=>'text', :size=>40, :name=>'title', :id=>'todo-title'}
%input{:type=>'submit', :value=>'New Todo'}
#content
%p== #{@todos.size} Todos
%table.display
%thead
%th{:>'80%'} Title
%th Controls
- @todos.each do |todo|
%tr{:class=>("complete" if todo.complete)}
%td{:class=>'title'}= todo.title
%td
%a{:class=>(todo.complete ? "#{todo.id}/mark/incomplete" : "#{todo.id}/mark/complete" )}= todo.complete ? "reopen" : "mark complete"
|
%a{:class=>("#{todo.id}/delete" )}= "delete"
%script
:plain
document.getElementById('todo-title').focus();
!!!
%html
%head
%title Todo
%link{:rel=>'stylesheet', :class=>'/stylesheets/style.css', :type => "text/css"}
%body
#banner My To Do List
= yield
.complete
:background #eee
.title
:text-decoration line-through
table.display
:border-collapse collapse
:width 100%
td
:border 1px solid gray
:padding 4px
#new-todo
:background #eee
:padding 10px
input
:font-size 18px
body
:font-family arial
:padding 0px
:margin 0px
#content
:padding 10px
#banner
:height 50px
:border-bottom 2px solid #777
:background #aab
:text-align center
:font-size 24px
Comments
Bala Paranj on Dec 19
I have a rake task that has recurring billing logic. The rake task loads the Rails environment and uses the Order model to charge customers by interacting with Payment Gateway.
If I write a Sinatra based app, will it require fewer resources?
Andre Lewis on Dec 20
Bala,
All other things being equal, the Sinatra app will start up much more quickly and use less memory than the Rails app.
You could also modify your rake task to *not* load up the entire rails environment, but just load the models it needs, establish the database connection on its own, etc.
katz on Aug 15
I'm sinking my teeth into learning Sinatra and Merb (in prep for Rails 3). Impressed on how simple a sinatra app is. It's like several files in Rails can just be reduced into one file.
Schneider on Apr 03
Very cool, helped me a lot :)
Jay Greasley on Apr 04
Hey,
Excellent post, really helped me get up to speed with Sinatra etc
I did find one bug, maybe because I'm running Sinatra 1.0
I had to change
todo.update_attributes(:complete=>(params[:is_complete]=='complete'))
to
todo.update(:complete=>(params[:is_complete]=='complete'))
jay greasley on Apr 05
doh! except of course that's the DataMapper method. SO maybe I'm running a newer version of DataMapper than you.
Post a comment
Stay in Touch
Last Five Posts
Rails on Ruby 1.9.1 in production: just do it
RubyMine + Snow Leopard
Business lessons learned
Load Averages, Explained
My Book
Categories
My WiFi site
Top cities: San Francisco WiFi; Portland WiFi; Charlotte WiFi
sinatra complete require simple deploy
for its contents. This is a safe-cache copy of the original web site.