pluggability

home

hg.sr.ht/~ged/Pluggability

docs

deveiate.org/code/pluggability

github

github.com/ged/pluggability

Description

Pluggability is a toolkit for creating plugins.

It provides a mixin that extends your class with methods to load and instantiate its subclasses by name. So instead of:

require 'acme/adapter/png'
png_adapter = Acme::Adapter::PNG.new( 'file.png' )

you can do:

require 'acme/adapter'
png_adapter = Acme::Adapter.create( :png, 'file.png' )

A full example of where this might be useful is in a program which generates output with a ‘driver’ object, which provides a unified interface but generates different kinds of output.

First the abstract base class, which is extended with Pluggability:

# in mygem/driver.rb:
require 'pluggability'
require 'mygem' unless defined?( MyGem )

class MyGem::Driver
    extend Pluggability
    plugin_prefixes "mygem/drivers"
end

We can have one driver that outputs PDF documents:

# mygem/drivers/pdf.rb:
require 'mygem/driver' unless defined?( MyGem::Driver )

class MyGem::Driver::PDF < Driver
    ...implementation...
end

and another that outputs plain ascii text:

#mygem/drivers/ascii.rb:
require 'mygem/driver' unless defined?( MyGem::Driver )

class MyGem::Driver::ASCII < Driver
    ...implementation...
end

Now the driver is configurable by the end-user, who can just set it by its short name:

require 'mygem'

config[:driver_type] #=> "pdf"
driver = MyGem::Driver.create( config[:driver_type] )
driver.class #=> MyGem::Driver::PDF

# You can also pass arguments to the constructor, too:
ascii_driver = MyGem::Driver.create( :ascii, :columns => 80 )

How Plugins Are Loaded

The create class method added to your class by Pluggability searches for your module using several different strategies. It tries various permutations of the base class’s name in combination with the derivative requested. For example, assume we want to make a LogReader base class, and then use plugins to define readers for different log formats:

require 'pluggability'

class LogReader
    extend Pluggability

    def read_from_file( path ); end
end

When you attempt to load the ‘apache’ logreader class like so:

LogReader.create( 'apache' )

Pluggability searches for modules with the following names:

ApacheLogReader
apachelogreader
apache_log_reader
apache

Obviously the last one might load something other than what is intended, so you can also tell Pluggability that plugins should be loaded from a subdirectory by declaring one or more plugin_prefixes in the base class. Each prefix will be tried (in the order they’re declared) when searching for a subclass:

class LogReader
    extend Pluggability
    plugin_prefixes 'drivers'
end

This will change the list that is required to:

drivers/apache_logreader
drivers/apache_LogReader
drivers/apachelogreader
drivers/apacheLogReader
drivers/apache

If you specify more than one subdirectory, each of them will be tried in turn:

class LogReader
    extend Pluggability
    plugin_prefixes 'drivers', 'logreader'
end

will change the search to include:

'drivers/apachelogreader'
'drivers/apache_logreader'
'drivers/apacheLogReader'
'drivers/apache_LogReader'
'drivers/ApacheLogReader'
'drivers/Apache_LogReader'
'drivers/apache'
'drivers/Apache'
'logreader/apachelogreader'
'logreader/apache_logreader'
'logreader/apacheLogReader'
'logreader/apache_LogReader'
'logreader/ApacheLogReader'
'logreader/Apache_LogReader'
'logreader/apache'
'logreader/Apache'

If the plugin is not found, a Pluggability::PluginError is raised, and the message will list all the permutations that were tried.

Preloaded Plugins

Sometimes you don’t want to wait for plugins to be loaded on demand. For that case, Pluggability provides the Pluggability#load_all method. This will find all possible matches for plugin files and load them, returning an Array of all the loaded classes:

class Template::Tag
    extend Pluggability
    plugin_prefixes 'tag'
end

tag_classes = Template::Tag.load_all

Excluding Some Files

You can also prevent some files from being automatically loaded by either Pluggability#create or Pluggability#load_all by setting one or more exclusion patterns:

LogReader.plugin_exclusions 'spec/*', %r{/test/}

The patterns can either be Regexps or glob Strings.

Logging

If you need a little more insight into what’s going on, Pluggability uses the Loggability library. Just set the log level to ‘debug’ and it’ll explain what’s going on:

require 'pluggability'
require 'loggability'

class LogReader
    extend Pluggability
end

# Global level
Loggability.level = :debug

# Or just Pluggability's level:
Pluggability.logger.level = :debug

LogReader.create( 'ringbuffer' )

this might generate a log that looks (something) like:

[...] debug {} -- Loading derivative ringbuffer
[...] debug {} -- Subdirs are: [""]
[...] debug {} -- Path is: ["ringbuffer_logreader", "ringbuffer_LogReader",
  "ringbufferlogreader", "ringbufferLogReader", "ringbuffer"]...
[...] debug {} -- Trying ringbuffer_logreader...
[...] debug {} -- No module at 'ringbuffer_logreader', trying the next alternative:
  'cannot load such file -- ringbuffer_logreader'
[...] debug {} -- Trying ringbuffer_LogReader...
[...] debug {} -- No module at 'ringbuffer_LogReader', trying the next alternative:
  'cannot load such file -- ringbuffer_LogReader'
[...] debug {} -- Trying ringbufferlogreader...
[...] debug {} -- No module at 'ringbufferlogreader', trying the next alternative:
  'cannot load such file -- ringbufferlogreader'
[...] debug {} -- Trying ringbufferLogReader...
[...] debug {} -- No module at 'ringbufferLogReader', trying the next alternative:
  'cannot load such file -- ringbufferLogReader'
[...] debug {} -- Trying ringbuffer...
[...] debug {} -- No module at 'ringbuffer', trying the next alternative:
  'cannot load such file -- ringbuffer'
[...] debug {} -- fatals = []
[...] error {} -- Couldn't find a LogReader named 'ringbuffer': tried 
  ["ringbuffer_logreader", "ringbuffer_LogReader", 
  "ringbufferlogreader", "ringbufferLogReader", "ringbuffer"]

Installation

gem install pluggability

Contributing

You can check out the current development source with Mercurial via its Mercurial repo. Or if you prefer Git, via its Github mirror.

After checking out the source, run:

$ rake newb

This task will install any missing dependencies, run the tests/specs, and generate the API documentation.

Authors

License

Copyright © 2008-2020, Michael Granger and Martin Chase All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

  • Neither the name of the author/s, nor the names of the project's contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.