TAG | ruby
I am spending most of my time at work these days on the UW ยท Forward project. One of the big features we are trying to bake into the application is the ability to place requests from one library to another or from campus to campus in the UW System. To accomplish this we are using the Voyager XML Over HTTP Web Services API. Using these web services feels right, like a nice sweet spot in the relationship between libraries and their ILS vendors.
The API works well, though my institution is only using Voyager 7.1 and so we are using one of the early versions of the APIs and it has parts that are not documented well or that are a bit sloppy. There are seeming mismatches between some of the XML values returned and the data the element names represent. For example, when placing a request there is an expiration date that is returned. However, the timestamp that is returned corresponds to the time the request was placed.
Additionally, it is difficult to find complete documentation from Ex Libris on all the different cases for which data is returned. When staff process a request made by a patron, there are many stages that the request steps through. The documentation is a little light on what numeric codes correspond meaningful stage names. Or certain elements appear and disappear as a request is put through different stages, but it is difficult to know this without nearly reverse engineering the process.
These are places that the API could be improved (and yes, documentation is just as important a piece of an API as the API code and requests/responses themselves). But overall, I am quite glad that our ILS vendor is putting these services in place. It enables us to embed ILS functionality in places for which it would be unreasonable to expect the vendors themselves to put them. The work we are doing on our Forward project can also be used in places like the campus portal, where people manage their other accounts associated with university life.
We are writing a Ruby plugin for the Voyager API. We are starting with the basic functions we need:
- authenticate a patron
- return his/her ILS account
- renew items
- place a request for an item
- cancel requests for an item
Our code is not fully baked itself and not documented yet, so there nothing to share at this time. We do intend to share it more broadly in the future.
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