Module: PluginFactory

Defined in:
lib/pluginfactory.rb

Overview

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 #=> "mysql" driver = Driver.create( config ) driver.class #=> MysqlDriver pgdriver = Driver.create( "PostGresDriver" )

Authors

License

Copyright (c) 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.

Constant Summary collapse

VERSION =

Library version

'1.0.8'

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.default_loggerObject

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



116
117
118
# File 'lib/pluginfactory.rb', line 116

def default_logger
  @default_logger
end

.loggerObject Also known as: log

The logger that's currently in effect



119
120
121
# File 'lib/pluginfactory.rb', line 119

def logger
  @logger
end

Class Method Details

.extend_object(obj) ⇒ Object

Add the @derivatives instance variable to including classes.



161
162
163
164
# File 'lib/pluginfactory.rb', line 161

def self::extend_object( obj )
	obj.instance_variable_set( :@derivatives, {} )
	super
end

.included(klass) ⇒ Object

Inclusion callback -- extends the including class. This is here so you can either 'include' or 'extend'.



155
156
157
# File 'lib/pluginfactory.rb', line 155

def self::included( klass )
	klass.extend( self )
end

.logger_callback=(callback) ⇒ Object

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



127
128
129
130
131
132
133
134
135
136
# 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_loggerObject

Reset the global logger object to the default



140
141
142
143
# File 'lib/pluginfactory.rb', line 140

def self::reset_logger
	self.logger = self.default_logger
	self.logger.level = Logger::WARN
end

.using_default_logger?Boolean

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

Returns:

  • (Boolean)


148
149
150
# File 'lib/pluginfactory.rb', line 148

def self::using_default_logger?
	return self.logger == self.default_logger
end

Instance Method Details

#create(class_name, *args, &block) ⇒ Object

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' )



256
257
258
259
260
261
262
263
264
265
266
# 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| /#{__FILE__}/ =~ frame}
		msg = "When creating '#{class_name}': " + err.message
		Kernel.raise( err, msg, nicetrace )
	end
end

#derivative_classesObject Also known as: derivativeClasses

Returns an Array of registered derivatives



238
239
240
# File 'lib/pluginfactory.rb', line 238

def derivative_classes
	self.derivatives.values.uniq
end

#derivativesObject

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



173
174
175
176
177
178
179
180
# 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

#factory_typeObject Also known as: factoryType

Returns the type name used when searching for a derivative.

Raises:



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# 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 =~ /^.*::(.*)/
		return $1
	else
		return base.name
	end
end

#get_module_name(class_name) ⇒ Object Also known as: getModuleName

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.



336
337
338
339
340
341
342
343
344
# File 'lib/pluginfactory.rb', line 336

def get_module_name( class_name )
	if class_name =~ /\w+#{self.factory_type}/
		mod_name = class_name.sub( /(?:.*::)?(\w+)(?:#{self.factory_type})/, "\\1" )
	else
		mod_name = class_name
	end

	return mod_name
end

#get_subclass(class_name) ⇒ Object Also known as: getSubclass

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.



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# 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

#inherited(subclass) ⇒ Object

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



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# 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( /#<Class:0x[[:xdigit:]]+>::/i, '' )
		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( /(?:.*::)?(\w+)(?:#{self.factory_type})/i )
			keys << Regexp.last_match[1].downcase
		else
			keys << subclass.name.sub( /.*::/, '' ).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

#load_derivative(class_name) ⇒ Object Also known as: loadDerivative

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.



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# 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

#make_require_path(modname, subdir) ⇒ Object Also known as: makeRequirePath

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"]


421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# 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

#require_derivative(mod_name) ⇒ Object Also known as: requireDerivative

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.



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# 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