Pluggability module

The Pluggability module

Constants

FactoryError

An exception class for Pluggability specific errors.

VERSION

Library version

Attributes

pluggable_classes R
derivatives R

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

Public Class Methods

extend_object( obj )

Add the @derivatives instance variable to including classes.

# File lib/pluggability.rb, line 23
def self::extend_object( obj )
        obj.instance_variable_set( :@plugin_prefixes, [] )
        obj.instance_variable_set( :@plugin_exclusions, [] )
        obj.instance_variable_set( :@derivatives, {} )

        Pluggability.pluggable_classes << obj

        super
end
plugin_base_class( subclass )

Return the ancestor of subclass that has Pluggability.

# File lib/pluggability.rb, line 41
def self::plugin_base_class( subclass )
        return subclass.ancestors.find do |klass|
                Pluggability.pluggable_classes.include?( klass )
        end
end

Public Instance Methods

create( class_name, *args, &block )

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/pluggability.rb, line 204
def create( class_name, *args, &block )
        subclass = get_subclass( class_name )

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

Returns an Array of registered derivatives

# File lib/pluggability.rb, line 187
def derivative_classes
        self.derivatives.values.uniq
end
factory_type()
Alias for: plugin_type
find_plugin_path( mod_name )

Search for the file that corresponds to mod_name using the plugin prefixes and current Gem load path and return the path to the first candidate that exists.

# File lib/pluggability.rb, line 357
def find_plugin_path( mod_name )
        candidates = self.plugin_path_candidates( mod_name )
        Pluggability.log.debug "Candidates for %p are: %p" % [ mod_name, candidates ]

        candidate_paths = candidates.
                flat_map {|path| Gem.find_latest_files( path ) }.
                reject {|path| self.is_excluded_path?( path ) || ! File.file?(path) }
        Pluggability.log.debug "Valid candidates in the current gemset: %p" % [ candidate_paths ]

        return candidate_paths.first
end
get_module_name( class_name )

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 plugin type in it (eg., ‘My::FooService’ for Service, or by appending the plugin type if it doesn’t.

# File lib/pluggability.rb, line 323
def get_module_name( class_name )
        if class_name =~ /\w+#{self.plugin_type}/
                mod_name = class_name.sub( /(?:.*::)?(\w+)(?:#{self.plugin_type})/, "\\1" )
        else
                mod_name = class_name
        end

        return mod_name
end
get_subclass( class_name )

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/pluggability.rb, line 220
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 PluginError,
                                "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
inherited( subclass )

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

# File lib/pluggability.rb, line 132
def inherited( subclass )
        plugin_class = Pluggability.plugin_base_class( subclass )

        Pluggability.logger.debug "%p inherited by %p" % [ plugin_class, subclass ]
        keys = [ subclass ]

        # If it's not an anonymous class, make some keys out of variants of its name
        if subclass.name
                keys += plugin_class.make_derivative_names( subclass )
        else
                Pluggability.log.debug "  no name-based variants for anonymous subclass %p" % [ subclass ]
        end

        keys.compact!
        keys.uniq!

        # Register it under each of its name variants
        keys.each do |key|
                Pluggability.log.debug "Registering %s derivative of %s as %p" %
                        [ subclass.name, plugin_class.name, key ]
                plugin_class.derivatives[ key ] = subclass
        end

        # Add a name attribute to it
        class << subclass
                attr_reader :plugin_name
        end
        subclass.instance_variable_set( :@plugin_name, keys.last )

        super
end
is_excluded_path?( path )

Returns true if any of the plugin_exclusions match the specified +path.

# File lib/pluggability.rb, line 92
def is_excluded_path?( path )
        rval = self.plugin_exclusions.find do |exclusion|
                case exclusion
                when Regexp
                        path =~ exclusion
                when String
                        flags = 0
                        flags &= File::FNM_EXTGLOB if defined?( File::FNM_EXTGLOB )
                        File.fnmatch( exclusion, path, flags )
                else
                        Pluggability.log.warn "Don't know how to apply exclusion: %p" % [ exclusion ]
                        false
                end
        end

        if rval
                Pluggability.log.debug "load path %p is excluded by %p" % [ path, rval ]
                return true
        else
                return false
        end
end
load_all()

Find and load all derivatives of this class, using plugin_prefixes if any are defined, or a pattern derived from the plugin_type if not. Returns an array of all derivative classes. Load failures are logged but otherwise ignored.

# File lib/pluggability.rb, line 250
def load_all
        Pluggability.log.debug "Loading all %p derivatives." % [ self ]
        patterns = []
        prefixes = self.plugin_prefixes

        if prefixes && !prefixes.empty?
                Pluggability.log.debug "Using plugin prefixes (%p) to build load patterns." % [ prefixes ]
                prefixes.each do |prefix|
                        patterns << "#{prefix}/*.rb"
                end
        else
                # Use all but the last pattern, which will just be '*.rb'
                Pluggability.log.debug "Using plugin type (%p) to build load patterns." %
                        [ self.plugin_type ]
                patterns += self.make_require_path( '*', '' )[0..-2].
                        map {|f| f + '.rb' }
        end

        patterns.each do |glob|
                Pluggability.log.debug "  finding derivatives matching pattern %p" % [ glob ]
                candidates = if Gem.respond_to?( :find_latest_files )
                                Gem.find_latest_files( glob )
                        else
                                Gem.find_files( glob )
                        end

                Pluggability.log.debug "  found %d matching files" % [ candidates.length ]
                next if candidates.empty?

                candidates.each do |path|
                        next if self.is_excluded_path?( path )
                        Kernel.require( path )
                end
        end

        return self.derivative_classes
end
load_derivative( class_name )

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

# File lib/pluggability.rb, line 296
def load_derivative( class_name )
        Pluggability.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.plugin_type,
                        class_name.downcase,
                ]
                Pluggability.log.error( errmsg )
                raise PluginError, errmsg, caller(3)
        end
end
make_derivative_names( subclass )

Return all variants of the name of the given subclass that can be used to load it.

# File lib/pluggability.rb, line 167
def make_derivative_names( subclass )
        keys = []

        simple_name = subclass.name.sub( /^.*::/i, '' ).sub( /\W+$/, '' )
        keys << simple_name << simple_name.downcase
        keys << simple_name.gsub( /([a-z0-9])([A-Z])/, "\\1_\\2" ).downcase

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

        return keys
end
make_require_path( modname, subdir )

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/pluggability.rb, line 385
def make_require_path( modname, subdir )
        path = []
        myname = self.plugin_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

        Pluggability.log.debug "Path is: #{path.uniq.reverse.inspect}..."
        return path.uniq.reverse
end
plugin_exclusions( *exclusions )

Get/set patterns which cause files in a plugin path to not be loaded. Typical use case is to exclude test/spec directories:

MyFactoryType.plugin_exclude( 'spec/**' )
# File lib/pluggability.rb, line 77
def plugin_exclusions( *exclusions )
        @plugin_exclusions.replace( exclusions ) if !exclusions.empty?
        return @plugin_exclusions
end
plugin_exclusions=( args )

Set the plugin exclusion patterns which cause files in a plugin path to not be loaded.

# File lib/pluggability.rb, line 85
def plugin_exclusions=( args )
        @plugin_exclusions = Array( args )
end
plugin_path_candidates( mod_name )

Return an Array of all the filenames a plugin of the given mod_name might map to given the current plugin_prefixes.

# File lib/pluggability.rb, line 372
def plugin_path_candidates( mod_name )
        prefixes = self.plugin_prefixes
        prefixes << '' if prefixes.empty?

        return prefixes.flat_map {|pre| self.make_require_path(mod_name, pre) }
end
plugin_prefixes( *args )

Get/set the prefixes that will be used when searching for particular plugins for the calling Class.

# File lib/pluggability.rb, line 59
def plugin_prefixes( *args )
        @plugin_prefixes.replace( args ) if !args.empty?
        return @plugin_prefixes
end
plugin_prefixes=( args )

Set the prefixes that will be used when searching for particular plugins for the calling Class.

# File lib/pluggability.rb, line 67
def plugin_prefixes=( args )
        @plugin_prefixes = Array( args )
end
plugin_type()

Returns the type name used when searching for a derivative.

# File lib/pluggability.rb, line 117
def plugin_type
        base = Pluggability.plugin_base_class( self ) or
                raise PluginError, "Couldn't find plugin base for #{self.name}"

        if base.name =~ /^.*::(.*)/
                return $1
        else
                return base.name
        end
end
Also aliased as: factory_type
require_derivative( mod_name )

Search for the module with the specified mod_name, using any plugin_prefixes that have been set. Return the path that was required.

# File lib/pluggability.rb, line 336
def require_derivative( mod_name )
        plugin_path = self.find_plugin_path( mod_name )
        unless plugin_path
                errmsg = "Couldn't find a %s named '%s': tried %p" % [
                        self.plugin_type,
                        mod_name,
                        self.plugin_path_candidates( mod_name )
                ]
                Pluggability.log.error( errmsg )
                raise Pluggability::PluginError, errmsg
        end

        Kernel.require( plugin_path )

        return plugin_path
end