Stepping off the Rails - adventures with Sinatra (Part 1)

In this episode, our trusty adventurer leaves the warm and fuzzy confines of Rails, and explores an ‘alternative’ Ruby web framework. Part 1 will cover the app itself, and Part 2 will cover deployment.

I must admit it: I’m one of those folks that came to Ruby because of Rails. And don’t get me wrong – I like Rails, but I frickin’ love Ruby. I’m not a clever person, but Ruby makes me feel clever every day. Ruby is a good friend, and I want to get to know it better outside of its relationship with Rails. I’m also interested in building simple web apps that don’t need the full weight of Rails. So I turned to Sinatra.

For those of you that don’t know all about it, Sinatra is a lightweight Ruby web application framework that takes a different approach than Rails. With Sinatra, you can write an entire web app in one file, with all of your templates inline (you can also have your templates in separate files instead, as we will see in a minute). Sinatra is rackable, and can be deployed in a Rack container, making it straightforward to deploy under Passenger (deployment is covered in Part 2). There are lots of other good intros out there on Sinatra, and I’ll list a few at the bottom of this post.

The Controller

Let’s get started! The sample application here is URLUnwind. You can see it in operation at urlunwind.com, and see the full source on github. It is a simple short URL unwinder, using Net::HTTP to make a HEAD request and then pulling out the ‘Location:’ header.

Sinatra apps are based around one controller file. You can break out your controller actions into separate files, but will need to manually require them into the primary controller. For this app, the controller is urlunwind.rb. Right now, we are just interested in the actions:

get '/' do
haml :index
end
 
post '/unwind' do
@url, @unwound_url, @unwind_error = unwind(params[:url])
haml :index
end
 
get '/unwind.json' do
content_type 'application/json'
 
@url, @unwound_url, @unwind_error = unwind(params[:url])
{ :from_url => @url, :to_url => @unwound_url, :error => @unwind_error }.to_json
end
 
get '/stylesheets/style.css' do
content_type 'text/css'
 
sass :style
end

To define an action, you use a simple DSL. It starts with the method (one of: ‘get’, ‘post’, ‘put’, ‘delete’, where ‘put’ and ‘delete’ are handled with a ‘_method’ parameter on a post), followed by the route that should trigger the action. The routes are searched in order they are defined until a match is found. You can include named parameters (with :symbol) or splat parameters (with *) in the routes as well (see the Sinatra page for examples of this). In a similar fashion to Rails, parameters matched in the route, provided in the post body, or included in the url all end up in a params hash available to your action.

If a request fails to match any route, Sinatra looks for a matching file path in a public/ directory in the same directory as the called executable ($0@) (this path can be overridden). I use that feature to provide a "SASS":http://haml.hamptoncatlin.com/docs/rdoc/classes/Sass.html stylesheet, but I have other stylesheets in @public/stylesheets/ that are served statically.

After the action does it’s business, whatever it returns is given to the client.

Rendering Views

View templates can be defined in four ways: inline on the render call, embedded in the controller, using the template method, or in individual files. I prefer the cleanliness of having a separate file for each view, so used that approach. Sinatra will look for file based views in a views/ directory in the same directory as the called executable ($0@) (this path can also be overridden). You render a template by calling a method for the type of template it is. This app has only two templates: @index.haml and style.sass (rendered with haml :index and sass :style, respectively).

As in Rails, ivars defined in the action are available within the template. You can also pass locals on the render call.

You can override the content-type for a particular action. I do that in the stylesheet and json actions with content_type.

Testing

Sinatra provides helpers for Test::Unit, Test::Spec, and RSpec. Here is a snippet from my Test::Unit tests:

 def test_index
get_it '/'
assert_match /Unwind It/, @response.body
end
 
def test_unwind_with_blank_url
post_it '/unwind', :url => ''
assert_match /Please enter/, @response.body
end

Sinatra provides helpers to call the actions: get_it, post_it, delete_it, put_it. After calling any of these, the response is available in the @respose ivar. The test helpers have a little growing to do – I was only able to assert_match against the body of the response. It would be nice to have access to the assigned ivars as well (this may be changed in edge, I built all of this against the 0.3.2 gem version of Sinatra).

Since Sinatra looks for views relative to the executable, calling the tests with ruby test/urlunwind_test.rb had it looking for test/views/, which did not exist. I linked test/views/ to ../views/, and all was good. You could also probably define a :test environment configuration that set the path to the views as well. I’ll cover overriding the default paths in Part 2, and you can read about configurations on the Sinatra site.

Project Structure

Here is a snap of the project structure:

layout I’ll discuss the purpose of the vendor/ and config.ru entries in Part 2 as part of my deployment discussion.

Resources

There are some good references out there for Sinatra (this list is by no means exhaustive):

* The Sinatra Homepage – In case you didn’t see the dozen links to it above

* The Sinatra Book – Which I discovered after writing this app. Wish I had seen it earlier.

* Chris Schneider’s blog – A good resource of Sinatra tips. Chris also maintains the Sinatra Book.

* #sinatra on irc.freenode.net – Folks there were quick to answer my deployment questions, and prodded me to provide a web service from the app.

* The Sinatra mailing list

* faithfulgeek’s ’10 Minute Web App’ screencast

* Update: Ruby Inside article – Sinatra: 29 Links and Resources For A Quicker, Easier Way to Build Webapps

I hate ruby

I love to read. In college, I had no social life for the first two years because I lived within walking distance of a 9-floor library. I always had a book in my pocket or in my hand, and read constantly. Sometimes I would read while walking to class. Then I got busy with college life, harder classes that required more coding and less reading. So reading became a hobby rather than an obsession.

After college, I took a job working on a large app written in C at a company 3000 miles from home. I had time to read again! Not only was there no homework, there was no social life, since it was a new town. I also had a bit of time to read at work while the app was building and tests were running.

After the C project, I moved onto a team doing Java. Now, in addition to the building/testing time for reading, I was forced to read! Everything I wanted to do required reading 2-3 javadoc pages, with a ‘choose your own adventure’ element. I would have an object of class A@, and what I really needed was an object of class @B. Starting at the doc for each end, I would try different paths to get to the other end, often backtracking at dead ends.

Then I moved into web development, and started with good ole’ PHP. I lost a lot of the api adventure from Java and lost the build/test reading breaks (testing?). But I gained more time for reading! ‘How?’ you ask? Job security! The code I wrote lacked cohesion, followed few standards, and was unmaintainable by anyone else. I became an essential part of the organization, and could come and go as I pleased. So I read in the morning, came in late, read at lunch, and left early to read back at home.

And then it all went to shit. I started programming in ruby.

No more api reading – I would guess at method names, try them out in irb, and be right most if the time. No more compile breaks. Testing? Yes, but with autotest, it was quick. I could barely find my place on the page before the tests were done. No more job security – I was writing code the ‘ruby way’, or the ‘rails way’, and any competent ruby/rails programmer could pick it up. So no more leaving early/arriving late. And coding was fun again! All those other languages felt too much like work. So no more reading at home either – just sitting in front of my laptop, hacking away on crap. Hell, I even started doing the opposite of reading; I started writing about ruby.

F you ruby.

Idle hands are the coder's tools

After being busy for what seems like forever, things have slowed down the last couple of weeks, and I’ve been able to get a few things done. Like putting up a terribly ugly placeholder site for Hand Built Software, and starting this blog.

I’ve also been trying to learn some new things and improve on some things I already kind of know. Last week I built a rails app from scratch, testing all the way. It was nice to truly be the boss of the project and call all of the shots. It was also nice to get back to basics with Test::Unit and do TDD from the start. It was nice to build most of the application without hitting it with the browser; the context switch of switching from Emacs to FireFox wears me out sometimes, and I really dislike using the mouse when I’m coding. The app is a simple secret gift exchange name chooser, with a twist. You can play with it at HatFullOfNames.com.

Last night and today I built an app using Sinatra, the lightweight Ruby web framework that is all the rage with the kids these days. It was a fun adventure – most of my Ruby experience so far has been within the Rails bubble. I plan to write a couple of posts about the experience – one covering working with Sinatra, the second covering deploying a Sinatra app to Passenger on shared hosting (Dreamhost in this case). This app is a short url unwinder, allowing you to see the destination of a short url before clicking on it. It also provides a web service to handle the lookups that returns JSON. Use it at URLUnwind.com.

I’ll get started on that Sinatra post right now…