Building a simple Sinatra/ DataMapper/ HAML/ SASS + Passenger app

Dec 18, 2008 by Andre

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.

How I organized my Sinatra App

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 main application file

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.

My config.ru

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

Notes on Rake

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

Notes on Capistrano

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

My View, Layout, and stylesheet

app/views/index.haml

#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();

app/views/layout.hml

!!!
%html
  %head
    %title Todo
    %link{:rel=>'stylesheet', :class=>'/stylesheets/style.css', :type => "text/css"}       
  %body
    #banner My To Do List
    = yield

app/views/style.sass

.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

What I learned ... how to:

  • create a simple web app in Sinatra
  • use DataMapper for an ORM
  • use HAML and SASS
  • run my app in development and production environments (production on Passenger)
  • deploy quick-and-easy without an SCM using capistrano's scp capability

Comments

1

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?

2

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.

3

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.

4

Schneider on Apr 03

Very cool, helped me a lot :)

5

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'))

6

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

spacer
spacer
Recommend me at WWR -->

Last Five Posts

A Sous Vide Cooking Experiment
Rails on Ruby 1.9.1 in production: just do it
RubyMine + Snow Leopard
Business lessons learned
Load Averages, Explained

My Book

Available now from Apress

Categories

  • Ajax
  • Cooking
  • Entrepreneurial
  • Gadgets
  • GeoKit
  • Google
  • Google Maps
  • How-to
  • Javascript
    • Prototype
    • jQuery
  • Misc
  • MySQL
  • Open source
  • OpenID
  • PlaceShout
  • Railsconf
  • Ruby
  • Ruby on Rails
  • SEO
  • Scout
  • ShapeWiki
  • Sinatra
  • Speaking / writing
  • Tools
    • JSLog
  • Usability
  • Web
  • Web 2.0
  • Wifi Cafes
  • git
  • iphone
  • system

My WiFi site

Top cities: San Francisco WiFi; Portland WiFi; Charlotte WiFi

 
This is so filters can reject the spam-bots. Thanks!
 
gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.