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:
This is just an old fashioned link log post. The following is a good series on functional loops implemented in Ruby by Rails Spikes:
- Functional programming and looping
- Understanding map and reduce
- MapReduce, with inspiration from functional programming