Website / Report Issue / Source Code ( Build Status )

Loadable

Safely Customize Ruby's Load System


1 Description

The Loadable gem provides a robust and convenient means of augmenting Ruby's load system, namely the load and require methods. Rather than alias and override these methods, Loadable keeps a list of load hooks (also called load wedges) that control the routing of require and load calls.

In addition, the Loadable gem includes two pre-made load hooks that can be used to prevent name clashes between Ruby's standard library and gem packages (see INFRACTIONS.md for more on this). There is also a load hook for developers to make vendored sub-projects loadable.

2 Features

  • Safely augment Ruby's load system.
  • Prevent library name clashes.
  • Search load locations.

3 Usage

3.1 Installation

Installing via RubyGems follows the usual pattern.

gem install loadable

To automatically load both the Gem and Ruby hooks, and the entire Loadable system, add loadable to your RUBYOPT environment variable.

export RUBYOPT="-rloadable"

Place this in your shell's configuration file, such as ~/.bashrc. For csh syntax (e.g. in ~/.cshrc) use:

setenv RUBYOPT "-rloadable"

If you do not want the default setup you can require 'loadable/system' instead. This will load in Loadable system, but only add an OriginalLoader to the $LOADERS list, leaving off the Ruby and Gem loaders.

3.2 Custom Loaders

Loadable was written initially to provide the specific capability of loading Ruby standard libraries without potential interference from libraries installed via RubyGems (see INFRACTIONS.md). The code ultimately evolved into a more generic tool, useful for writing any kind of plug-in load router.

The code for the Ruby hook serves as a good example of writing a load hook. (Note this is leaves out a few details of the real class for simplicity's sake.)

require 'rbconfig'
require 'loadable/mixin'

class Loadable::RubyLoader
  include Loadable

  LOCATIONS = ::RbConfig::CONFIG.values_at(
    'rubylibdir', 'archdir', 'sitelibdir', 'sitearchdir'
  )

  def call(fname, options={})
    return unless options[:from].to_s == 'ruby'
    LOCATIONS.each do |loadpath|
      if path = lookup(loadpath, fname, options)
        return super(path, options)
      end
    end
    raise_load_error(fname)
  end

  def each(options={}, &block)
    LOCATIONS.each do |loadpath|
      traverse(loadpath, &block)
    end
  end
end

To put this loader into action we simply need to register it with the Loadable domain.

Loadable.register(Loadable::RubyLoader.new)

Under the hood, this simply appends the instance to the $LOADERS global variable.

Loaders, also called load hooks or wedges, are easy to write as their interface is very simple. Any object that responds to #call, taking parameters of (fname, options={}), can be used as a load hook. A load hook should also support #each(options={}, &block) which is used to iterate over all requirable files a loader supports.

The Loadable mixin is just a convenience module that makes writing loaders a bit easier. Load hooks can be written without it, however the mixin provides a few methods that are often useful to any load hook. An example is the lookup method used in the above example, which will search a load path in accordance with the Ruby's built-in require and load lookup procedures, i.e. automatically trying default extensions like .rb.

You might wonder how the single method, #call, handles both load and require operations. The secret is in the options hash. If options[:load] resolves to true, then it is a load operation, otherwise it is a require operation. The $LOADERS global variable is iterated over in order. When #load or #require is called each hook is tried in turn. The return value of #call controls how this loop proceeds. If the return value is true then the load was successful, and the loop can break. If it is false it means the loading has already been handled and the loop can also break. But if the return value is nil, it means the hook does not apply and the loop should continue. If all hooks have been tried and all have returned nil then it falls back to the original #load and #require calls, via an instance of OriginalLoader which should always be the last loader in the $LOADERS list.

4 Built-in Loaders

The Loadable gem provides three special loaders out-of-the-box: the RubyLoader, the GemLoader and the VendorLoader. The first two are probably not exactly what you think they are, going just by their names, so keep reading...

4.1 RubyLoader

The Ruby hook makes it possible to load a Ruby standard library without interference from installed gems or other package systems. It does this by checking for a :from option passed to the require or load methods.

require 'ostruct', :from=>'ruby'

This will load the ostruct.rb script from the Ruby standard library regardless of whether someone else dropped an ostruct.rb file in their project's lib/ directory without understanding the potential consequences.

4.2 GemLoader

The Gem hook is similar to the Ruby hook, in that it isolates the loading of a gem's files from other gems.

gem 'facets', '~>2.8'

require 'string/margin', :from=>'facets'

With this we can be sure that 'facets/string/margin' was loaded from the Facets library regardless of whether some other gem has a 'facets/string/margin' file in its lib/ directory. If no gem has this file, it will fallback to the remaining loaders. However, if we use the :gem options instead, it will raise a load error.

require 'string/does_not_exit', :gem=>'facets'

The Gem hook also supports version constraints, so you do not have to use gem() method for one-off requires from a given gem.

require 'string/margin', :from=>'facets', :version=>'~>2.8'

4.3 VendorLoader

The Vendor hook is used to add vendored projects to the load system. This is especially useful for development. Vendored projects can be added in two ways, by registering an instance of VendorLoader, e.g.

Loadable.register Loadable::VendorLoader.new('vendor/*')

Or using the dedicated Loadable.vendor(*dir) method that Loadable provides to make this more convenient.

Loadable.vendor('vendor/*')

5 Development

Source code for Loadable is hosted by GitHub.

If you come across any issues, we encourage you to fork the repository and submit a pull request with the fix. When submitting a pull request, it is best if the changes are organized into a new topic branch.

If you don't have time to code up patches yourself, please do not hesitate to simply report the issue on the issue tracker.

6 Copyrights

Loadable if copyrighted open source software.

Copyright (c) 2010 Rubyworks

Load is distributed under the terms of the BSD-2-Clause license.

See LICENSE.txt file for details.