I love Sinatra. I have been able to make some great little apps with it, including this site. I have a few goals that I want to achieve right now and that is to use Sinatra in conjunction with Nanite to spawn mini apps to handle worker reporting via AJAX (or via AMQP) to a main app. I am still thinking about the architecture. I have the components working but I’m still sorting out how to fit them together in a simple but robust way.
Be that as it may, my main point for writing this article today was to talk about the basic elements of a Sinatra app. There are three elements that make up a dynamic website and this has been made evident to me over and over again. Those three elements are model, view and controller, MVC programing pattern. I am not going to discuss the pattern or the different variations, I will just be talking about how I implement/interpret it. For the Model aspect I use a variety of ORMs, ActiveRecord, Datamapper and Sequel. I will talk about each and how I use them in Sinatra. For the View I use HTML, javascript and CSS. Not much I am willing say about this here, as it’s out of the scope (i.e. not Ruby), but I will be talking about Compass and how I use it to compile my CSS files. For the Controller I use Sinatra, Merb or Ruby on Rails. I will only be talking about Sinatra here though.
ORMs
Straight up, I will only use ActiveRecord if I am working on a legacy Rails application. Though the new AR is looking good thanks to the Rails3 development, I usually avoid AR and will use Datamapper most of the time. DM has a really efficient compilation of SQL and is not that far from AR because of the RESTful focus and is almost the same as far as the most common uses. Plus I like the properties being in the ‘class’ code (makes versioning of DB schema a little easier/clear without worrying about migrations). Sequel I have only just started using this one but I see it as better at reporting. Though it is much less magical then AR and DM, I like it a hell of a lot, as I am an old school straight up SQL lover.
ActiveRecord, I don’t have much to say about this guy other than I try to restrict my use of it as much as possible. I hardly ever succeed though, hehe. This has a very strong user base and is robust in its feature set. I just find that it is a CPU hog as you can’t always pair it down to one call to the DB for anything that gets more complex than a simple find. So when you are doing some complex data manipulation you can end up with an overwhelming number of calls to the DB, slowing things down considerably. As you can tell I am having a hard time being objective about this ORM but in all honesty it gets the job done but there are other better options out there.
Datamapper, my favorite ORM at the moment. I landed on using DM because it is the default ORM of Merb. I had been having issues with Rails restrictive behavior when it came to doing anything out of the box. I found with Merb I could cut down to the size I needed. Yet, what I liked about it even more was that I had to get closer to pure Ruby. This lead me to Sinatra. The ability to easily decouple DM and use it anywhere was key to my liking it. I have a few issues with DM as it’s just a little magical in how it creates SQL. So to figure out how to get a join going and get access to DB server side procedures is a bitch and you can end up still writing pure SQL. Try like heck to not do that though.
So here comes Sequel to the rescue. For simple data access it is a little excessive but when you start to touch on complexity in the data model you see a the magical curtain hiding the Wizard of OZ disappear. It allows you to trust the DB server with what it does best. Running SQL commands to pull data out of the database. Basically Sequel is a way to build complex SQL commands with Ruby.
Sinatra, How I use it
Sinatra RB is one of the best ways to make a mini web app. You only have 2 pages in the application but you still need dynamic abilities. Merb and Rails are way too complex for your needs. Sinatra comes to the rescue. You can place your entire app in one file and just deploy it like so:
require 'rubygems'
require 'sinatra'
get '/hi' do
"Hello World!"
end
and on the command line …
$ gem install sinatra
$ ruby hi.rb
== Sinatra has taken the stage ...
>> Listening on 0.0.0.0:4567
Bam! You have yourself a web app. It’s great but not very flexible and if you’re doing anything moderately complex, a single file gets just a tad overwhelming. Well right off the bat the most common web app for a programmer to make is a Blog. In fact I made this blog with Sinatra, surprise! But I needed a few custom things that your basic blog will not come with but I did not need any of the crazy over head that blog software like Wordpress have. First thing I did was to separate all the elements of Sinatra into more manageable pieces. It does not come with model functionality, so I got to choose anyone I wanted and in this circumstance I wanted to use CouchDB, a blog is just a collection of documents. So what did was I separate them into? Models, Views (native functionality), Helpers and Controller (routes.rb). Here is the default structure that I work with:
app.rb
config.ru
helpers.rb
models.rb
public/
+- favicon.ico
+- images/
+- javascripts/
+- application.js
+- index.js
+- jquery.js
routes.rb
views/
+- index.erb
+- layout.erb
+- sass/
+- main.sass
This is the set up that I use for a server with Passenger. The app.rb file is what I use to gather in all the dependancies. I do not have a vendor directory, as I usually have access to server root (I install the gems) but if you do not have that type of access you should create one and install your gems there. I am considering making this a gem so that I can quicken my project startups.
# app file ========================================================
# GEMS
require 'rubygems'
require 'sinatra'
require 'erb' # for templates
require 'compass' # for stylesheets
require 'haml' # for compass
require 'pony' # for email alerts
require 'ostruct' # for configuration ease
require 'uri' # for formating URI
require 'curb' # for DLing webpages faster
require 'curb-fu' # wrapper for curb
# LOCAL LIBs
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/lib')
require 'authorization' # self explanitory
require 'exceptions' # custom exceptions
require 'wowarmory' # processes XML data DLed from wowarmory.com
configure do
# configure compass
Compass.configuration do |config|
config.project_path = File.dirname(__FILE__)
config.sass_dir = File.join(Sinatra::Application.views, 'sass')
config.output_style = :compact
end
end
# load application parts ==========================================
load 'models.rb'
load 'helpers.rb'
load 'routes.rb'
This is a very important file where it all comes together. You have your gems, your local libraries, your configuration (VERY simple in this case) and your loading all the application parts. Why do I require all my gems here? Well to make easier to keep track of. I have ONE exception that I have for this is the models.rb file. I do place all the configuration and require settings in that file so that I can use the file stand alone in irb. This allows me to test data access and manipulation when in development. Also this comes in handy is when I am working with different ORMs. Ar times I go one step further separating the models into a different files for each ORM (i.e. models.sequel.rb, models.datamapper.rb or models.couchrest.rb). This is helpful when you’re accessing the same DB (MySQL, PostgreSQL or CouchDB) in different ways. This goes into my philosophy that you should always cut things up into manageable pieces that allows you to consume an application one mouthful at a time.
Models
As I have said I place all the configuration info into this file so that I can use it in multiple places including other apps if the need arises (_it happens!_). Here is a small snippet:
# models file =======================================================
require 'rubygems'
require 'ostruct'
# database ORMs
require 'couchrest'
# model configuration ===============================================
# configure blog
Security = OpenStruct.new(
:database_name => 'security',
:url_base_database => 'http://localhost:XXXX/'
)
# user model ========================================================
# uses CouchRest (CouchDB)
class User < CouchRest::ExtendedDocument
use_database CouchRest.database!((Security.url_base_database || '') + Security.database_name)
unique_id :email
property :email
property :password
property :name
view_by :created_at, :descending=>true
view_by :email
view_by :id
timestamps!
couchrest_type = 'User'
end
... SNIP ...
I also have some Datamapper stuff in this file but what is here is enough to understand what I am doing.
Helpers
This is in a seperate file only because I do not want ANY code in the app.rb file. There is just one method that I want to talk about in this file and that is
def get_js_tag(names)
names.split(",").collect{ |name|
"<script type=\"text/javascript\" src=\"/attendance/js/#{name}.js\"></script>" if File.exist?("#{options.root}/public/js/#{name}.js")
}.compact.join("\n")
end
This make life so much easier when working with javascript in complex ways. And will only include the file in your HTML if it exists. This feeds into the chop it up thoughts about digestible code. It also helps with avoiding collision with variables and such. Yes, this is what namespacing is for but remember “digestible code”. I am not saying to not use namespacing, you should use it. But this allows you to be much more succinct. Not to mention when working in a team this allows for better tracking when you are using versioning possibly avoiding annoying conflicts.
Routes
Here is the heartbeat of the application. This is where the majority of your code will go. I make sure that I am very deliberate with my routing names. You can mimic Rails naming with controllerName/actionName or create a much more complex URI as your app needs. What you need to be aware of here is that if it is not here it does not exist. With things like Merb or Rails you get a whole slew of default routes that you do not necessarily need or want in the case of resources. Of note when creating these the order they appear in the route file will greatly influence which route get run.
get '/' do
@name = "index" # this is for including the JS file for this page, exclude this variable if you don't need it
# set page data for pagination
@pagedata = { :cur_page => 1, :post_count => 0, :per_page => 3 }
# set the current page to show
@pagedata[:cur_page] = params[:page].to_i unless params[:page].nil?
@posts = Post.by_created_at
# set post_count
@pagedata[:post_count] = @posts.size
@posts = @posts.paginate(:page => @pagedata[:cur_page], :per_page => @pagedata[:per_page]) unless @posts.empty?
erb :index
end
As you can see here I have a @name instance variable that is used for the get_js_tag helper method in the ERB index file.
How do I handle CSS? I use SASS processed threw Compass. But what is great about Sinatra routing is that all my sass files get output as css files very easily with this route below.
get '/css/:name.css' do
content_type 'text/css', :charset => 'utf-8'
sass :"sass/#{params[:name]}", :sass => Compass.sass_engine_options
end
I do nto need to do anything other then make sure that I link to the css file in the ERB temples.
One minor pitfall that you will run into with forms is just remembering that you need to use get and post route depending on how you submit data to the web app. I know it sounds simple but it’s a “DOH!” moment that you should watch for.
Conclusion
While you know know what I do with Sinatra please make sure that you go over the documentation repeatedly. It is surprisingly complete. You will also find a lot of starter apps on github that will give you a good idea how others use this great tool.
Keep your code in manageable bites. I know that we all start out with one file and as we code that file will enlarge. Do not let this happen to you. Things will quickly twist around and as time goes on your naming convention might change and finding things will quickly get out of hand. Organization is key, so keep code in groups that work together. There are 4 thing that I separate all this into. Models, Helpers (misc. code snippets that get used in multiple places), Routes (your controller) and Libraries. I do not go into libraries very much here but there are whole sections of code that you use throughout your app that is more then an action and defiantly more then a helper. Think of it as a Gem but one that you made.
Well that’s all folks. Hope that all this helps you out on a small or even a large project.