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[:driver_type] #=> “mysql” driver = Driver.create( config[:driver_type] ) driver.class #=> MysqlDriver pgdriver = Driver.create( “PostGresDriver” )

Authors

  • Martin Chase
  • Michael Granger

:include: LICENSE


Please see the file LICENSE for licensing details.

Constant Summary

VERSION =

Library version

'1.0.7'

Class Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Attribute Details

+ (Object) default_logger

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



92
93
94
# File 'lib/pluginfactory.rb', line 92

def default_logger
  @default_logger
end

+ (Object) logger Also known as: log

The logger that’s currently in effect



95
96
97
# File 'lib/pluginfactory.rb', line 95

def logger
  @logger
end

Class Method Details

+ (Object) extend_object(obj)

Add the @derivatives instance variable to including classes.



137
138
139
140
# File 'lib/pluginfactory.rb', line 137

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

+ (Object) included(klass)

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



131
132
133
# File 'lib/pluginfactory.rb', line 131

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

+ (Object) log



96
97
98
# File 'lib/pluginfactory.rb', line 96

def logger
  @logger
end

+ (Object) log=



97
98
99
# File 'lib/pluginfactory.rb', line 97

def logger=(value)
  @logger = value
end

+ (Object) logger_callback=(callback)

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



103
104
105
106
107
108
109
110
111
112
# File 'lib/pluginfactory.rb', line 103

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

+ (Object) reset_logger

Reset the global logger object to the default



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

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

+ (Boolean) using_default_logger?

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

Returns:

  • (Boolean)


124
125
126
# File 'lib/pluginfactory.rb', line 124

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

Instance Method Details

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


232
233
234
235
236
237
238
239
240
241
242
# File 'lib/pluginfactory.rb', line 232

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

- (Object) derivative_classes Also known as: derivativeClasses

Returns an Array of registered derivatives



214
215
216
# File 'lib/pluginfactory.rb', line 214

def derivative_classes
  self.derivatives.values.uniq
end

- (Object) derivativeClasses



217
218
219
# File 'lib/pluginfactory.rb', line 217

def derivative_classes
  self.derivatives.values.uniq
end

- (Object) derivatives

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



149
150
151
152
153
154
155
156
# File 'lib/pluginfactory.rb', line 149

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

- (Object) factory_type Also known as: factoryType

Returns the type name used when searching for a derivative.

Raises:



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/pluginfactory.rb', line 160

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

- (Object) factoryType



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/pluginfactory.rb', line 179

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

- (Object) get_module_name(class_name) 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.



312
313
314
315
316
317
318
319
320
# File 'lib/pluginfactory.rb', line 312

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

- (Object) get_subclass(class_name) 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.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/pluginfactory.rb', line 248

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

- (Object) getModuleName



321
322
323
324
325
326
327
328
329
# File 'lib/pluginfactory.rb', line 321

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

- (Object) getSubclass



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

- (Object) inherited(subclass)

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



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/pluginfactory.rb', line 184

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

- (Object) load_derivative(class_name) 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.



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/pluginfactory.rb', line 284

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

- (Object) loadDerivative



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/pluginfactory.rb', line 305

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

- (Object) make_require_path(modname, subdir) 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"]


397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/pluginfactory.rb', line 397

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

- (Object) makeRequirePath



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'lib/pluginfactory.rb', line 420

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

- (Object) require_derivative(mod_name) 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.



327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
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
# File 'lib/pluginfactory.rb', line 327

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

- (Object) requireDerivative



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/pluginfactory.rb', line 389

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