zerosum dirt(nap)

evolution through a series of accidents

zerosum dirt(nap)

Clone Pastie in 15 Minutes with Sinatra & DataMapper

November 05, 2007 by nap · Comments

Hey there, put on your tutorial hat. Today we’re going to learn how to write a dirt simple pastie clone using Sinatra and Datamapper. For those of you unfamiliar with pastie systems, they’re commonly used in IRC to paste bits of code to a globally-visible place outside of the channel. So you copy some code from your IDE, paste it into a web page, submit it, and the Pastie system fancies it up with some syntax highlighting or whatnot. You can then copy the resultant URL and paste it back into the channel so other people can view it.

Examples of this type of service include pastie.caboo.se and Nopaste @ rafb.net. Those sites do a lot of extra cool stuff, like allow you to select the language for different syntax highlighting, select themes for viewing, and so on. But the core concept itself isn’t a terribly complex one, and our example is going to be about as barebones as they come. The interesting thing isn’t the application we’re going to build here so much as it is the tools we’ll use. That is, we’ll be using the pastie example to introduce you to two cool new pieces of Ruby tech: Sinatra, a new web micro-framework, and the DataMapper ORM package.

Sinatra, on the surface, is a lot like Camping, another Ruby web all-in-one-file micro-framework. Camping hasn’t seen any active development in quite some time though, and the syntax can be a bit strange for new users. Sinatra is much more straight-forward and accessible. It’s really a kind of domain-specific language for writing simple web applications, used to define RESTful actions and how they should be handled. This makes it perfect for lightweight mini-apps. It’s also completely ORM-agnostic, instead of being married to ActiveRecord like both Rails and Camping are. For more information, check out the ‘official’ tutorial.

DataMapper is the new ORM package on the block, and an alternative to ActiveRecord (and Og and Sequel and so on). It just moved into town but it’s already sitting at the cool kid lunch table. Whereas AR implements the ActiveRecord Pattern, DataMapper (surprise!) implements the DataMapper pattern. There are a number of reasons why I prefer this approach, but that’s probably fodder for an entirely separate blog post, so I won’t get into that here. Besides, the team has already written a great summary of why DataMapper rocks. Read it. Oh, and performance kicks ass now too.

OK, anyway. Tutorial time. Found your plastic hat? Good. Let’s go. First let’s get the gems we’ll need. As of this writing, DM is at v0.2.3 and Sinatra is at v0.1.7. We’re also going to retrieve the Syntaxi gem, which we’ll use for syntax highlighting.

sudo gem install sinatra datamapper json syntaxi -y

Since DM uses the DataObjects.rb drivers, you’ll want to install them. They’re packaged with the distribution. For our purposes here we’re going to assume you’re on MySQL but all the standard drivers are there, so don’t fret. Change to your DM gem directory (mine is /opt/local/lib/ruby/gems/1.8/gems/datamapper-0.2.3) and issue the following command. Ignore any warnings that are generated. If you’re on OS X 10.5, you may want to check out Heimidal’s blog for instructions.

sudo rake dm:install:mysql

Now that our prerequisites are satisfied, let’s get started by creating the file toopaste.rb:

require 'rubygems'
require 'sinatra'
require 'data_mapper'
require 'syntaxi'

### SETUP

DataMapper::Database.setup({
  :adapter  => 'mysql',
  :host     => 'localhost',
  :username => 'root',
  :password => '',
  :database => 'toopaste_development'
})

### MODELS

class Snippet < DataMapper::Base
  property :body, :text
  property :created_at, :datetime
  property :updated_at, :datetime

  validates_presence_of :body
  validates_length_of :body, :minimum => 1

  Syntaxi.line_number_method = 'floating'

  def formatted_body
    html = Syntaxi.new("[code lang='ruby']#{self.body}[/code]").process
    "<div class=\"syntax syntax_ruby\">#{html}</div>"
  end
end

database.table_exists?(Snippet) or database.save(Snippet)

### CONTROLLER ACTIONS

layout 'default.erb'

# new
get '/' do
  erb :new, :layout => 'default.erb'
end

# create
post '/' do
  @snippet = Snippet.new(:body => params[:snippet_body])
  if @snippet.save
    redirect "/#{@snippet.id}"
  else
    redirect '/'
  end
end

# show
get '/:id' do
  @snippet = Snippet.find(params[:id])
  erb :show, :layout => 'default.erb'
end

Next we’ll dissect this code listing to give you a feel for how Sinatra and DataMapper work, and show the code listings for our views as we get to them.

First of all, we define our database connection for DataMapper. Looks pretty similar to its AR equivalent, eh? Before we can run this we’ll need to create the MySQL database mentioned in the source. You can create the toopaste_development database by issuing the following mysql command:

mysqladmin -u root -p create toopaste_development

We’re not going to define a migration here, as this is a very simple case, but DM does support migrations in case you’re curious. We next define our model, Snippet, which inherits from DataMapper::Base. The validators we’re specifying look familiar to anyone coming over from AR but the property declarations might look a little bit odd at first.. OMG I don’t have to put comments in my source file to remind me what attributes are available on my models?!

property :body, :text
property :created_at, :datetime
property :updated_at, :datetime

It’s like a little slice of heaven, isn’t it?

Skip over the formatted_body method for a moment and look at the line after the model class declaration that checks for pre-existence of the table. You’ll note that if it’s not found we call database.save with the model name as a parameter. This creates our table, with all the appropriate fields, as specified in the model.

database.table_exists?(Snippet) or database.save(Snippet)

The formatted_body method is simple. It just takes the body of our snippet (a property on the model) and wraps it in some code that gives us pretty syntax highlighting and line numbering. Syntaxi leverages Jamis Buck’s Syntax gem to mark up the specified code, wrapping CSS span tags around things that should be colored, adding line numbers, and some other goodies too. You’ll see the CSS in our layout shortly.

Another interesting DM optimization worth noting: by default, the text field (body) is lazily loaded. Text columns are expensive in databases, and by using lazy loading, we only access them when they’re needed. This speeds things up significantly in most cases. However, if you don’t like it, you can just pass a :lazy => false option in the text column property declaration.

Moving on to the controller actions, you’ll see that routes and actions are intimately married in Sinatra. Although this isn’t desirable for a larger application, it’s great for quickie one-off web apps like this pastie project. We only have three actions and they’re only ever available at these three URL patterns.

get '/' do
  erb :new, :layout => 'default.erb'
end

The first action displays a form to create a new paste, and it’s always available at ‘/’, your application root. Note that it’s a GET request. The body of this action renders the new.erb template, which should be in your views/ subdirectory. Instead of pulling in an external template, you could just stash your HTML inline here. This may work fine for dirt-simple applications but I prefer to keep it separate. Here’s that first view, /views/new.erb:

<div class="snippet">
  <form action="/" method="POST">
    <textarea name="snippet_body" id="snippet_body" rows="20"></textarea>
    <br/><input type="submit" value="Save"/>
  </form>
</div>

The next action is analogous to a #create action in RESTful Rails. The action is requested by a POST to the application root. Sinatra supports the standard GET and POST HTTP methods as well as PUT and DELETE, meaning that you can build RESTful applications with it.

post '/' do
  @snippet = Snippet.new(:body => params[:snippet_body])
  if @snippet.save
    redirect "/#{@snippet.id}"
  else
    redirect '/'
  end
end

Pretty standard stuff, right? We retrieve the parameter posted from the submit action in the new paste form, instantiate a new model and try to save it. If the validations pass, we redirect to the #show action equivalent. If not, we’re just going to dump you back to the #new form again. Since the only way the action will fail is if the body property is empty, we’re not going to bother with any sort of error message at this time. We’re not rendering anything here (merely redirecting), so no template is required.

If our post is successful, we’re going to be taken to the #show action, which lives at /:id, where :id is the primary key of the corresponding database record. This action will also get accessed directly when you paste that URL to the chat room, and people click to view your code.

get '/:id' do
  @snippet = Snippet.find(params[:id])
  erb :show, :layout => 'default.erb'
end

In the code listing above, we look up the particular snippet specified in params[:id] and set an instance variable. We then render an ERb template, which of course has access to that instance variable. Here’s the code you’ll want in /views/show.erb:

<div class="snippet">
  <div class="sbody" id="content"><%= @snippet.formatted_body %></div>
  <div class="sdate">Created on <%= @snippet.created_at.strftime("%B %d, %Y at %I:%M %p") %></div>
  <br/><a href="/">New Paste!</a>
</div>

If you’ve been paying attention you’ve noticed that both of our erb method calls (used to render an embedded ruby HTML view, you can also use haml) have specified a layout. That layout does the sorts of things a layout usually does; it sets up the page body, the title of the page, the styles, and so on. For completeness’ sake, we’ll list it here:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title><%= @title || 'Toopaste!' %></title>
  <style>
    html {
      background-color: #eee;
    }
    .snippet {
      margin: 5px;
    }
    .snippet textarea, .snippet .sbody {
      border: 5px dotted #eee;
      padding: 5px;
      width: 700px;
      color: #fff;
      background-color: #333;
    }
    .snippet textarea {
      padding: 20px;
    }
    .snippet input, .snippet .sdate {
      margin-top: 5px;
    }

    /* Syntax highlighting */
    #content .syntax_ruby .normal {}
    #content .syntax_ruby .comment { color: #CCC; font-style: italic; border: none; margin: none; }
    #content .syntax_ruby .keyword { color: #C60; font-weight: bold; }
    #content .syntax_ruby .method { color: #9FF; }
    #content .syntax_ruby .class { color: #074; }
    #content .syntax_ruby .module { color: #050; }
    #content .syntax_ruby .punct { color: #0D0; font-weight: bold; }
    #content .syntax_ruby .symbol { color: #099; }
    #content .syntax_ruby .string { color: #C03; }
    #content .syntax_ruby .char { color: #F07; }
    #content .syntax_ruby .ident { color: #0D0; }
    #content .syntax_ruby .constant { color: #07F; }
    #content .syntax_ruby .regex { color: #B66; }
    #content .syntax_ruby .number { color: #FF0; }
    #content .syntax_ruby .attribute { color: #7BB; }
    #content .syntax_ruby .global { color: #7FB; }
    #content .syntax_ruby .expr { color: #909; }
    #content .syntax_ruby .escape { color: #277; }
    #content .syntax {
      background-color: #333;
      padding: 2px;
      margin: 5px;
      margin-left: 1em;
      margin-bottom: 1em;
    }
    #content .syntax .line_number {
      text-align: right;
      font-family: monospace;
      padding-right: 1em;
      color: #999;
    }
  </style>
</head>
<body>
  <%= yield %>
</body>
</html>

Save that listing as views/default.erb. The rendered page content will be inserted into this layout where it yields. And that’s pretty much it. You can fire up your new Sinatra and DataMapper-powered application by issuing the following command:

ruby toopaste.rb
== Sinatra has taken the stage on port 4567!

Sinatra sits on top of Mongrel, making it super-easy to use (and thread-safe to boot!). If you open up a web browser and point it at http://localhost:4567 you’ll see the results. You now have a fully functional (albeit slightly retarded) pastie clone with syntax highlighting for Ruby code snippets. And the core logic is all contained in a single file, with a few external ERb templates for cleanliness.

If you like, you can play with the finished app or download the sources. Enjoy, and please comment if you have any problems or suggestions for improvement. This code was written and tested on OS X 10.4 and Debian Etch.

UPDATED 07/02/08: I finally got around to updating this tutorial for DataMapper 0.9.2. About time, eh? Click here to see the new version.

blog comments powered by Disqus