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.
14 comments so far ↓
ScottG // November 05, 2007 @ 08:48 PM
Just a note for Ubuntu Linux users - I was unable to run the rake dm:install:mysql command, as it complained about a missing mysql_config utility. This utility comes with the libmysqlclient15-dev package, which can be installed easily with synaptic.
Once installed, I could execute the rake task without any errors.
ruby licious // November 05, 2007 @ 10:41 PM
Great post, I've been following sinatra with interest lately, rails feels way too bloated for certain projects.
Eivind Uggedal // November 08, 2007 @ 07:32 PM
Sinatra is brilliant for small projects. I just completed a very minimal blog app with it: http://redflavor.com/reprise.rb wich I'm using here: http://journal.redflavor.com
Justin Forder // November 09, 2007 @ 08:05 AM
Thank you!
I had just discovered Sinatra (via the InfoQ article "The Forgotten Ruby Web Frameworks") http://www.infoq.com/news/2007/11/forgotten-ruby-web-frameworks and you have now introduced me to DataMapper. I like the fact that DataMapper manages object identity properly, and the way it treats the Ruby DSL as the 'master' representation of the model.
A couple of points about the tutorial:
(1) you could point out that users can see Sinatra's error handling by putting a non-existent ID in the URL, and leave fixing this as an exercise for the reader :-)
(2) Toopaste doesn't cope well with toopaste.rb itself (see http://paste.zerosum.org/108). The occurrence of "[/code]" within toopaste.rb is being seen by Syntaxi as the end of the code it is coloring.
But it is great to see a minimal database-backed MVC example using Sinatra and DataMapper... my next step should be to try an example with multiple related model classes, using HAML for rendering.
nap // November 09, 2007 @ 11:50 AM
@justin -- absolutely, both good points. The syntaxi code block thing is particularly annoying. But being an introductory tutorial I wanted to keep things as simple as possible and focus on Sinatra and DataMapper rather than making it overlong by dealing with the edge cases. The pastie app itself is far from production-ready but it's good for illustrating how to get started with these things. Thanks for checking it out!
vwduder // November 09, 2007 @ 09:17 PM
On Ubuntu Gutsy, I also had to modify the datamapper rake file to require 'rubygems' at the top.
Ryan Allen // November 09, 2007 @ 11:22 PM
Cheers for the post. It's good to start seeing more lightweight alternatives to Rails for Ruby web application development.
And in particular, thread-safe ones :)
Sinatra is built on top of Rack, which you didn't mention:
http://rack.rubyforge.org/
For anyone that is interested, Rack allows you to build your own Ruby web frameworks with minimal fuss, I did a presentation on it, slides are here:
http://yeahnah.org/files/rack-presentation-oct-07.pdf
So, yeah hopefully we'll see an emergence of web frameworks now pushing other ideas in the space, that up until recently, Rails has been dominating in the Ruby community :)
nap // November 10, 2007 @ 01:15 AM
@ryan -- oops yeah forgot to mention Rack, thanks for pointing that out.
curious readers might also want to take a look at Ramaze, another Ruby web framework built on top of Rack.
Justin Forder // November 11, 2007 @ 09:45 PM
There's a little more documentation on Sinatra appearing at http://www.xnot.org/sinatra/
@ryan - great Rack presentation! On my MacBook Pro, running ab against your three-line 'Hello world' example on Rack, I get 1800 requests/sec with concurrency 1, 1500 with concurrency 10, 1000 with concurrency 50, and 850 with concurrency 100 - but with concurrency 100, ab is using at least as much CPU as ruby, so I think I need to test with ab on a separate box to get dependable results.
meekish // November 13, 2007 @ 11:55 PM
Cool tutorial. I think I've read enough good things about DataMapper to give it a serious look.
As an aside, I saw this in your example:
@database.table_exists?(Snippet) or database.save(Snippet)@
and wondered if this is just another way of writing:
@database.save(Snippet) unless database.table_exists?(Snippet)@
Or if it would behave differently. I'm always interested in new idioms ;)
meekish // November 13, 2007 @ 11:58 PM
I thought this thing was "textile":http://hobix.com/textile enabled?
"Code phrases can be surrounded by at-symbols."
john // November 14, 2007 @ 03:39 PM
@ScottG: Cheers for the Ubuntu knowledge. Was stuck at that.
Nate Murray // February 23, 2008 @ 11:01 AM
The newest version of datamapper (0.2.5) doesn't seem to work with this code. It gives weird errors such as:
bq. ERROR: key Snippet/Validatable::ValidatesPresenceOf/body must be unique, provide the :key option to specify a unique key
Downgrading to 0.2.3 works fine however!
nap // February 23, 2008 @ 05:13 PM
@nate: You're absolutely right. DM 0.9 should be released in March (with a whole bunch of other tasty changes), and I'll make sure to update this tutorial as soon as that's happened.
Leave a Comment