Circulatable: a Librarian’s Group

Because sometimes you need to trammel the editor and exorcise the rules of grammar…

TAG | ruby

Last week a colleague introduced me to Sinatra, a lightweight web app framework for Ruby. The Sinatra website describes it as, “a Domain Specific Language (DSL) for quickly creating web-applications in Ruby.”

Just as David Berman urges residents to “leave Kentucky, come to Tennessee,” I can urge my shop to finally ♫ leave PHP, come to Ruby ♫. I have no problem with PHP, I would just like to move to Ruby for the small stuff as well as full-blown Rails apps. I have been looking for something to write simple one-off Ruby apps with, the kind of project that does not require a full Rails application because, for example, it requires no ORM as it has no database. Usually these one-offs were the kind of things I would punt over to PHP.

The particular case in which I am employing Sinatra is a one page web form for paying library fines with a credit card. Our campus has a central credit card payment vendor, so all we need to do is log someone in and figure out how much he owes according to our ILS. The form action submits somewhere else so we don’t need a full web app. We do need to take the logged in user’s ID and query our ILS through its API to get the fine amount. So we will wrap this web form in a campus login and before writing the form, make an HTTP call to the ILS to prepopulate the fine field.

The Sinatra app looks like the following:

./:
-rw-r--r--@ 1 myuser  admin   336 Dec  2 16:21 config.ru
-rw-r--r--@ 1 myuser  mygroup   623 Dec  3 07:54 application.rb
drwxr-xr-x  5 myuser  mygroup   170 Dec  2 13:10 lib
drwxr-xr-x  6 myuser  mygroup   204 Dec  2 16:22 public
drwxr-xr-x  4 myuser  mygroup   136 Dec  2 16:13 tmp
drwxr-xr-x  5 myuser  mygroup   170 Dec  2 13:10 views

./lib:
-rw-r--r--@ 1 myuser  mygroup  585 Dec  2 13:10 authenticate_patron.rb
-rw-r--r--  1 myuser  mygroup  441 Dec  2 13:10 my_account_service.rb

./public:
-rw-r--r--@ 1 myuser  mygroup    19 Dec  2 13:10 index.html

./tmp:

./views:
-rw-r--r--@ 1 myuser  mygroup  3552 Dec  2 13:10 layout.haml
-rw-r--r--@ 1 myuser  mygroup  2761 Dec  2 13:10 payfines.haml

Here is a breakdown of the files involved, getting the simple stuff out of the way first.

Rack & the ./tmp and ./public directories

Sinatra can be deployed as a Rack based app. On our servers we will run this application through Passenger/ModRails, so the tmp directory exists primarily for bouncing the app via

  $ touch tmp/restart.txt

The public directory is what Passenger uses for the application root in the Sinatra app. We are deploying to a sub-URI on the server so we have the following in the apache conf:

  RackBaseURI /fees

And in the document root for the server a symbolic link to point to the public directory:

  $ ls -l /path/to/apache/docroot
  lrwxr-xr-x myuser mygroup somedate fees -> /path/to/sinatra/webapp/public

The ./views directory

I place the HTML views in this location. I use a layout file for the full template for my website with a yield just like I would in a Rails app. The other file, payfines, is a view that corresponds to a matching route defined in the ./application.rb. This info renders the HTML form.

The ./lib directory

I am using this location to keep files that map the XML responses I expect to get back from my ILS into objects that will be available in my payfines view. I am using HappyMapper. The great thing about Sinatra is that you can require 'rubygems' and then use any Gem in your application.

The app itself and deploying

application.rb

The following is my first version of the application file itself. It is in need of error handling and refactoring, but I include it to show just how simple an app can be.

# application.rb
require 'rubygems'
require 'sinatra'
require 'haml'
require 'happymapper'
require 'lib/authenticate_patron'
require 'lib/my_account_service'
require 'net/http'

get '/payfines' do
  h = Net::HTTP.new('localhost')
  hresp, @auth_patron_xml = h.get('/fees/auth-patron-response.xml', nil)
  @patron = AuthenticatePatron::ServiceData.parse(@auth_patron_xml, :single => true)
  hresp, account_xml = h.get("/fees/my-account-response.xml?patronId=#{@patron.patron_identifier.patron_id}&patronHomeUbId=YYYY", nil)
  @account = MyAccountService::ServiceData.parse(account_xml, :single => true)
  haml :payfines
end

config.ru

The final piece is to create a configuration file for Rack. The Sinatra Book has a section on deploying to Passenger. You can also Google for examples like the following:

, Hide

This is just an old fashioned link log post. The following is a good series on functional loops implemented in Ruby by Rails Spikes:

, Hide