Dealing with $LOAD_PATH properly
I recently went through a process of consolidating a few backend miniapps that power some boring parts of Lighthouse and Tender. I upgraded one app to Sinatra 1.0, and converted another from Rails to Sinatra. The goal was to mount them in the same rack process, therefore simplifying the deployment process all around. Doing this reinforced Ryan's sage advice about requiring rubygems in your libraries.
With libraries, it's cake. Your gem requirements are light. No one is deploying your libraries as-is, so you can assume that any configuration is handled in their applications. I'm still struggling with tests a bit, however.
- Do not require rubygems (or rip, bundler, etc) in any files in
libortest. - Do not mess with the load path either.
Applications are a different matter. These typically will be deployed, so some kind of configuration file is essential. I try to provide examples so coworkers can get up and running really quickly. My config files typically look something like this:
# config.rb
$LOAD_PATH << ... # for setting up the Sinatra app's `lib` path and any
# vendored libraries
require 'rubygems' # you can replace this with Bundler, Rip, etc
gem 'sinatra', '~> 1.0.0'
gem ...
require 'my-sinatra-app'
Now, when I re-package these in a different setting (such as when I mash two Sinatra apps into the same Rack process), I have full control over the $LOAD_PATH and the loaded gem versions.
One pattern I've adopted for apps using Sequel is some kind of #load method. I had problems where my code was loading Sequel::Model instances before the database configuration was setup. Requiring these files first would access the non-existent database configuration and blow up.
# OLD
require 'my-app' # requires 'sequel' and 'my-app/foo_model'
Sequel.db = '...'
# NEW
require 'my-app'
MyApp.load do
Sequel.db = '...'
end
# implementation
def self.load
require 'sequel'
yield
require 'my-app/foo_model'
end
For what it's worth, I've started using autoload more lately. That negates the problem completely.
# my-app.rb
require 'sequel'
module MyApp
autoload :FooModel, 'my-app/foo_model'
# config.rb
require 'my-app'
Sequel.db = '...'
MyApp::FooModel.do_something
This is Sinatra-specific, but always subclass from Sinatra::Base. I opt for the classic Sinatra style a lot because it's so convenient. But once I have something running and tested, I make it a full class.
- Using the classic style adds a lot of crufty methods to every object. This can cause problems in mid to large projects.
- You can easily isolate and test these Sinatra classes with Rack Test. Resque's Server provides a good sample implementation with tests.
class MyAppTest < Test::Unit::TestCase
include Rack::Test::Methods
def app
MyApp::Api # subclasses Sinatra::Base
end
This problem also extends to libraries using Sinatra. At first, I couldn't figure out why one of my older Sinatra apps still used the classic Sinatra DSL. I got my answer when I converted it: ClassyResources was including itself into main. I was not too pleased.
I'm assuming this code pre-dated Sinatra's excellent extension API, so I spent an hour registering the modules as proper Sinatra extensions. I was glad I could focus my programmer rage into a good learning process.
My TwitterServer library serves as a good example of well-tested a Sinatra extension.
Following these guidelines, I was able to load both of the Sinatra apps together with a simple 3-line rackup file.
require 'config'
use MiniApp1
run MiniApp2
# thin -R config.ru start
My only suggestion if you come across crappy libraries that muck with your $LOAD_PATH is to fork away and push any patches upstream. Sorry in advance if it's one of mine :)
What good libraries out there handle this poorly? Which ones are shining examples? How do you handle similar issues?