module PluginFactory

This module contains the PluginFactory mixin. Including PluginFactory in your class turns it into a factory for its derivatives, capable of searching for and loading them by name. This is useful when you have an abstract base class which defines an interface and basic functionality for a part of a larger system, and a collection of subclasses which implement the interface for different underlying functionality.

An example of where this might be useful is in a program which talks to a database. To avoid coupling it to a specific database, you use a Driver class which encapsulates your program’s interaction with the database behind a useful interface. Now you can create a concrete implementation of the Driver class for each kind of database you wish to talk to. If you make the base Driver class a PluginFactory, too, you can add new drivers simply by dropping them in a directory and using the Driver’s create method to instantiate them:

Creation Argument Variants

The create class method added to your class by PluginFactory searches for your module using

Synopsis

in driver.rb:

require "PluginFactory"

class Driver
        include PluginFactory
        def self::derivative_dirs
           ["drivers"]
        end
end

in drivers/mysql.rb:

require 'driver'

class MysqlDriver < Driver
        ...implementation...
end

in /usr/lib/ruby/1.8/PostgresDriver.rb:

require 'driver'

class PostgresDriver < Driver
        ...implementation...
end

elsewhere

require 'driver'

config[:driver_type] #=> "mysql"
driver = Driver.create( config[:driver_type] )
driver.class #=> MysqlDriver
pgdriver = Driver.create( "PostGresDriver" )

Authors

License

Copyright © 2008-2012 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.

Constants

VERSION

Library version

Attributes

default_logger[RW]

The logger that will be used when the logging subsystem is reset

log[RW]

The logger that’s currently in effect

log=[RW]

The logger that’s currently in effect

logger[RW]

The logger that’s currently in effect

Public Class Methods

extend_object( obj ) click to toggle source

Add the @derivatives instance variable to including classes.

# File lib/pluginfactory.rb, line 161
def self::extend_object( obj )
        obj.instance_variable_set( :@derivatives, {} )
        super
end
included( klass ) click to toggle source

Inclusion callback – extends the including class. This is here so you can either ‘include’ or ‘extend’.

# File lib/pluginfactory.rb, line 155
def self::included( klass )
        klass.extend( self )
end
logger_callback=( callback ) click to toggle source

Deprecated: use the Logger object at log to manipulate logging instead of this method.

# File lib/pluginfactory.rb, line 127
def self::logger_callback=( callback )
        if callback.nil?
                self.logger.formatter = nil
        else
                self.logger.formatter = lambda {|lvl, _, _, msg|
                        callback.call(lvl.downcase.to_sym, msg)
                        ''
                }
        end
end
reset_logger() click to toggle source

Reset the global logger object to the default

# File lib/pluginfactory.rb, line 140
def self::reset_logger
        self.logger = self.default_logger
        self.logger.level = Logger::WARN
end
using_default_logger?() click to toggle source

Returns true if the global logger has not been set to something other than the default one.

# File lib/pluginfactory.rb, line 148
def self::using_default_logger?
        return self.logger == self.default_logger
end

Public Instance Methods

create( class_name, *args, &block ) click to toggle source

Given the class_name of the class to instantiate, and other arguments bound for the constructor of the new object, this method loads the derivative class if it is not loaded already (raising a LoadError if an appropriately-named file cannot be found), and instantiates it with the given args. The class_name may be the the fully qualified name of the class, the class object itself, or the unique part of the class name. The following examples would all try to load and instantiate a class called “FooListener” if Listener included Factory

obj = Listener.create( 'FooListener' )
obj = Listener.create( FooListener )
obj = Listener.create( 'Foo' )
# File lib/pluginfactory.rb, line 256
def create( class_name, *args, &block )
        subclass = get_subclass( class_name )

        begin
                return subclass.new( *args, &block )
        rescue => err
                nicetrace = err.backtrace.reject {|frame| %r#{__FILE__}/ =~ frame}
                msg = "When creating '#{class_name}': " + err.message
                Kernel.raise( err, msg, nicetrace )
        end
end
derivativeClasses() click to toggle source
Alias for: derivative_classes
derivative_classes() click to toggle source

Returns an Array of registered derivatives

# File lib/pluginfactory.rb, line 238
def derivative_classes
        self.derivatives.values.uniq
end
Also aliased as: derivativeClasses
derivatives() click to toggle source

Return the Hash of derivative classes, keyed by various versions of the class name.

# File lib/pluginfactory.rb, line 173
def derivatives
        ancestors.each do |klass|
                if klass.instance_variables.include?( :@derivatives ) ||
                   klass.instance_variables.include?( "@derivatives" )
                        return klass.instance_variable_get( :@derivatives )
                end
        end
end
factoryType() click to toggle source
Alias for: factory_type
factory_type() click to toggle source

Returns the type name used when searching for a derivative.

# File lib/pluginfactory.rb, line 184
def factory_type
        base = nil
        self.ancestors.each do |klass|
                if klass.instance_variables.include?( :@derivatives ) ||
                        klass.instance_variables.include?( "@derivatives" )
                        base = klass
                        break
                end
        end

        raise FactoryError, "Couldn't find factory base for #{self.name}" if
                base.nil?

        if base.name =~ %r^.*::(.*)/
                return $1
        else
                return base.name
        end
end
Also aliased as: factoryType
getModuleName( class_name ) click to toggle source
Alias for: get_module_name
getSubclass( class_name ) click to toggle source
Alias for: get_subclass
get_module_name( class_name ) click to toggle source

Build and return the unique part of the given class_name either by stripping leading namespaces if the name already has the name of the factory type in it (eg., ‘My::FooService’ for Service, or by appending the factory type if it doesn’t.

# File lib/pluginfactory.rb, line 336
def get_module_name( class_name )
        if class_name =~ %r\w+#{self.factory_type}/
                mod_name = class_name.sub( %r(?:.*::)?(\w+)(?:#{self.factory_type})/, "\\1" )
        else
                mod_name = class_name
        end

        return mod_name
end
Also aliased as: getModuleName
get_subclass( class_name ) click to toggle source

Given a class_name like that of the first argument to create, attempt to load the corresponding class if it is not already loaded and return the class object.

# File lib/pluginfactory.rb, line 272
def get_subclass( class_name )
        return self if ( self.name == class_name || class_name == '' )
        if class_name.is_a?( Class )
                return class_name if class_name <= self
                raise ArgumentError, "%s is not a descendent of %s" % [class_name, self]
        end

        class_name = class_name.to_s

        # If the derivatives hash doesn't already contain the class, try to load it
        unless self.derivatives.has_key?( class_name.downcase )
                self.load_derivative( class_name )

                subclass = self.derivatives[ class_name.downcase ]
                unless subclass.is_a?( Class )
                        raise FactoryError,
                                "load_derivative(%s) added something other than a class "                                         "to the registry for %s: %p" %
                                [ class_name, self.name, subclass ]
                end
        end

        return self.derivatives[ class_name.downcase ]
end
Also aliased as: getSubclass
inherited( subclass ) click to toggle source

Inheritance callback – Register subclasses in the derivatives hash so that ::create knows about them.

# File lib/pluginfactory.rb, line 208
def inherited( subclass )
        keys = [ subclass ]

        # If it's not an anonymous class, make some keys out of variants of its name
        if subclass.name
                simple_name = subclass.name.sub( %r#<Class:0x[[:xdigit:]]+>::/, '' )
                keys << simple_name << simple_name.downcase

                # Handle class names like 'FooBar' for 'Bar' factories.
                PluginFactory.log.debug "Inherited %p for %p-type plugins" % [ subclass, self.factory_type ]
                if subclass.name.match( %r(?:.*::)?(\w+)(?:#{self.factory_type})/ )
                        keys << Regexp.last_match[1].downcase
                else
                        keys << subclass.name.sub( %r.*::/, '' ).downcase
                end
        else
                PluginFactory.log.debug "  no name-based variants for anonymous subclass %p" % [ subclass ]
        end

        keys.compact.uniq.each do |key|
                PluginFactory.log.info "Registering %s derivative of %s as %p" %
                        [ subclass.name, self.name, key ]
                self.derivatives[ key ] = subclass
        end

        super
end
loadDerivative( class_name ) click to toggle source
Alias for: load_derivative
load_derivative( class_name ) click to toggle source

Calculates an appropriate filename for the derived class using the name of the base class and tries to load it via require. If the including class responds to a method named derivativeDirs, its return value (either a String, or an array of Strings) is added to the list of prefix directories to try when attempting to require a modules. Eg., if class.derivativeDirs returns ['foo','bar'] the require line is tried with both 'foo/' and 'bar/' prepended to it.

# File lib/pluginfactory.rb, line 308
def load_derivative( class_name )
        PluginFactory.log.debug "Loading derivative #{class_name}"

        # Get the unique part of the derived class name and try to
        # load it from one of the derivative subdirs, if there are
        # any.
        mod_name = self.get_module_name( class_name )
        result = self.require_derivative( mod_name )

        # Check to see if the specified listener is now loaded. If it
        # is not, raise an error to that effect.
        unless self.derivatives[ class_name.downcase ]
                errmsg = "Require of '%s' succeeded, but didn't load a %s named '%s' for some reason." % [
                        result,
                        self.factory_type,
                        class_name.downcase,
                ]
                PluginFactory.log.error( errmsg )
                raise FactoryError, errmsg, caller(3)
        end
end
Also aliased as: loadDerivative
makeRequirePath( modname, subdir ) click to toggle source
Alias for: make_require_path
make_require_path( modname, subdir ) click to toggle source

Make a list of permutations of the given modname for the given subdir. Called on a DataDriver class with the arguments ‘Socket’ and ‘drivers’, returns:

["drivers/socketdatadriver", "drivers/socketDataDriver",
 "drivers/SocketDataDriver", "drivers/socket", "drivers/Socket"]
# File lib/pluginfactory.rb, line 421
def make_require_path( modname, subdir )
        path = []
        myname = self.factory_type

        # Make permutations of the two parts
        path << modname
        path << modname.downcase
        path << modname                              + myname
        path << modname.downcase       + myname
        path << modname.downcase       + myname.downcase
        path << modname                        + '_' + myname
        path << modname.downcase + '_' + myname
        path << modname.downcase + '_' + myname.downcase

        # If a non-empty subdir was given, prepend it to all the items in the
        # path
        unless subdir.nil? or subdir.empty?
                path.collect! {|m| File.join(subdir, m)}
        end

        PluginFactory.log.debug "Path is: #{path.uniq.reverse.inspect}..."
        return path.uniq.reverse
end
Also aliased as: makeRequirePath
requireDerivative( mod_name ) click to toggle source
Alias for: require_derivative
require_derivative( mod_name ) click to toggle source

If the factory responds to the derivative_dirs method, call it and use the returned array as a list of directories to search for the module with the specified mod_name.

# File lib/pluginfactory.rb, line 351
def require_derivative( mod_name )

        # See if we have a list of special subdirs that derivatives
        # live in
        if ( self.respond_to?(:derivative_dirs) )
                subdirs = self.derivative_dirs

        elsif ( self.respond_to?(:derivativeDirs) )
                subdirs = self.derivativeDirs

        # If not, just try requiring it from $LOAD_PATH
        else
                subdirs = ['']
        end

        subdirs = [ subdirs ] unless subdirs.is_a?( Array )
        PluginFactory.log.debug "Subdirs are: %p" % [subdirs]
        fatals = []
        tries  = []

        # Iterate over the subdirs until we successfully require a
        # module.
        subdirs.collect {|dir| dir.strip}.each do |subdir|
                self.make_require_path( mod_name, subdir ).each do |path|
                        PluginFactory.log.debug "Trying #{path}..."
                        tries << path

                        # Try to require the module, saving errors and jumping
                        # out of the catch block on success.
                        begin
                                require( path.untaint )
                        rescue LoadError => err
                                PluginFactory.log.debug "No module at '%s', trying the next alternative: '%s'" %
                                        [ path, err.message ]
                        rescue Exception => err
                                fatals << err
                                PluginFactory.log.error "Found '#{path}', but encountered an error: %s\n\t%s" %
                                        [ err.message, err.backtrace.join("\n\t") ]
                        else
                                PluginFactory.log.info "Loaded '#{path}' without error."
                                return path
                        end
                end
        end

        PluginFactory.log.debug "fatals = %p" % [ fatals ]

        # Re-raise is there was a file found, but it didn't load for
        # some reason.
        if fatals.empty?
                errmsg = "Couldn't find a %s named '%s': tried %p" % [
                        self.factory_type,
                        mod_name,
                        tries
                  ]
                PluginFactory.log.error( errmsg )
                raise FactoryError, errmsg
        else
                PluginFactory.log.debug "Re-raising first fatal error"
                Kernel.raise( fatals.first )
        end
end
Also aliased as: requireDerivative