1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00
puma--puma/lib/pluginfactory.rb
zedshaw 6120599b9c Created the project page dripping with sappy honey goodness.
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@33 19e92222-5c0b-0410-8929-a290d50e31e9
2006-02-12 19:31:28 +00:00

384 lines
11 KiB
Ruby

#!/usr/bin/env ruby -w
#
# 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 <tt>create</tt> 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::derivativeDirs
# ["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" )
#
#---##########
#
# == Rcsid
#
# $Id: pluginfactory.rb 32 2005-03-05 00:21:05Z ged $
#
# == Authors
#
# * Martin Chase <stillflame@FaerieMUD.org>
# * Michael Granger <ged@FaerieMUD.org>
#
# modified temporarily by Zed A. Shaw until license attribution can be given.
#---
#
# Please see the file docs/COPYRIGHT for licensing details.
#
### An exception class for PluginFactory specific errors.
class FactoryError < RuntimeError
def initialize( *args )
if ! args.empty?
msg = args.collect {|a| a.to_s}.join
super( msg )
else
super( message )
end
end
end # class FactoryError
### A mixin that adds PluginFactory class methods to a base class, so that
### subclasses may be instantiated by name.
module PluginFactory
### A callback for logging the various debug and information this module
### has to log. Should take two arguments, the log level, possibly as a
### symbol, and the log message itself.
@logger_callback = nil
class << self
attr_accessor :logger_callback
end
### If the logger callback is set, use it to pass on a log entry. First argument is
def self::log(level, *msg)
@logger_callback.call(level, msg.join) if @logger_callback
end
### Inclusion callback -- extends the including class.
def self::included( klass )
klass.extend( self )
end
### Raise an exception if the object being extended is anything but a
### class.
def self::extend_object( obj )
unless obj.is_a?( Class )
raise TypeError, "Cannot extend a #{obj.class.name}", caller(1)
end
obj.instance_variable_set( :@derivatives, {} )
super
end
#############################################################
### M I X I N M E T H O D S
#############################################################
### Return the Hash of derivative classes, keyed by various versions of
### the class name.
def derivatives
ancestors.each {|klass|
if klass.instance_variables.include?( "@derivatives" )
break klass.instance_variable_get( :@derivatives )
end
}
end
### Returns the type name used when searching for a derivative.
def factoryType
base = nil
self.ancestors.each {|klass|
if klass.instance_variables.include?( "@derivatives" )
base = klass
break
end
}
raise FactoryError, "Couldn't find factory base for #{self.name}" if
base.nil?
if base.name =~ /^.*::(.*)/
return $1
else
return base.name
end
end
### Inheritance callback -- Register subclasses in the derivatives hash
### so that ::create knows about them.
def inherited( subclass )
keys = [ subclass.name, subclass.name.downcase, subclass ]
# Handle class names like 'FooBar' for 'Bar' factories.
if subclass.name.match( /(?:.*::)?(\w+)(?:#{self.factoryType})/i )
keys << Regexp.last_match[1].downcase
else
keys << subclass.name.sub( /.*::/, '' ).downcase
end
keys.uniq.each {|key|
#PluginFactory::log :info, "Registering %s derivative of %s as %p" %
# [ subclass.name, self.name, key ]
self.derivatives[ key ] = subclass
}
super
end
### Returns an Array of registered derivatives
def derivativeClasses
self.derivatives.values.uniq
end
### Given the <tt>className</tt> 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 <tt>args</tt>. The <tt>className</tt>
### 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' )
def create( subType, *args, &block )
subclass = getSubclass( subType )
return subclass.new( *args, &block )
rescue => err
nicetrace = err.backtrace.reject {|frame| /#{__FILE__}/ =~ frame}
msg = "When creating '#{subType}': " + err.message
Kernel::raise( err.class, msg, nicetrace )
end
### Given a <tt>className</tt> 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.
def getSubclass( className )
return self if ( self.name == className || className == '' )
return className if className.is_a?( Class ) && className >= self
unless self.derivatives.has_key?( className.downcase )
self.loadDerivative( className )
unless self.derivatives.has_key?( className.downcase )
raise FactoryError,
"loadDerivative(%s) didn't add a '%s' key to the "\
"registry for %s" %
[ className, className.downcase, self.name ]
end
subclass = self.derivatives[ className.downcase ]
unless subclass.is_a?( Class )
raise FactoryError,
"loadDerivative(%s) added something other than a class "\
"to the registry for %s: %p" %
[ className, self.name, subclass ]
end
end
return self.derivatives[ className.downcase ]
end
### Calculates an appropriate filename for the derived class using the
### name of the base class and tries to load it via <tt>require</tt>. If
### the including class responds to a method named
### <tt>derivativeDirs</tt>, 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
### <tt>class.derivativeDirs</tt> returns <tt>['foo','bar']</tt> the
### require line is tried with both <tt>'foo/'</tt> and <tt>'bar/'</tt>
### prepended to it.
def loadDerivative( className )
className = className.to_s
#PluginFactory::log :debug, "Loading derivative #{className}"
# Get the unique part of the derived class name and try to
# load it from one of the derivative subdirs, if there are
# any.
modName = self.getModuleName( className )
self.requireDerivative( modName )
# Check to see if the specified listener is now loaded. If it
# is not, raise an error to that effect.
unless self.derivatives[ className.downcase ]
raise FactoryError,
"Couldn't find a %s named '%s'. Loaded derivatives are: %p" % [
self.factoryType,
className.downcase,
self.derivatives.keys,
], caller(3)
end
return true
end
### Build and return the unique part of the given <tt>className</tt>
### 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.
def getModuleName( className )
if className =~ /\w+#{self.factoryType}/
modName = className.sub( /(?:.*::)?(\w+)(?:#{self.factoryType})/, "\\1" )
else
modName = className
end
return modName
end
### If the factory responds to the #derivativeDirs method, call
### it and use the returned array as a list of directories to
### search for the module with the specified <tt>modName</tt>.
def requireDerivative( modName )
# See if we have a list of special subdirs that derivatives
# live in
if ( self.respond_to?(:derivativeDirs) )
subdirs = self.derivativeDirs
subdirs = [ subdirs ] unless subdirs.is_a?( Array )
# If not, just try requiring it from $LOAD_PATH
else
subdirs = ['']
end
fatals = []
# Iterate over the subdirs until we successfully require a
# module.
catch( :found ) {
subdirs.collect {|dir| dir.strip}.each do |subdir|
self.makeRequirePath( modName, subdir ).each {|path|
#PluginFactory::log :debug, "Trying #{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 ScriptError,StandardError => 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 :debug,
# "Found '#{path}'. Throwing :found"
throw :found
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?
#PluginFactory::log :debug, "Re-raising first fatal error"
Kernel::raise( fatals.first )
end
nil
}
end
### 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"]
def makeRequirePath( modname, subdir )
path = []
myname = self.factoryType
# Make permutations of the two parts
path << modname
path << modname.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
return path.uniq.reverse
end
end # module Factory