pluggability 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ == v0.0.1 [2012-08-03] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ First release after renaming from PluginFactory.
4
+
data/Manifest.txt ADDED
@@ -0,0 +1,8 @@
1
+ ChangeLog
2
+ History.rdoc
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/pluggability.rb
7
+ spec/lib/helpers.rb
8
+ spec/pluggability_spec.rb
data/README.rdoc ADDED
@@ -0,0 +1,237 @@
1
+ = pluggability
2
+
3
+ * http://deveiate.org/projects/Pluggability
4
+
5
+
6
+ == Description
7
+
8
+ Pluggability is a mixin module that turns an including class into a factory for
9
+ its derivatives, capable of searching for and loading them by name. This is
10
+ useful when you have an abstract base class which defines an interface and basic
11
+ functionality for a part of a larger system, and a collection of subclasses
12
+ which implement the interface for different underlying functionality.
13
+
14
+ An example of where this might be useful is in a program which talks to a
15
+ database. To avoid coupling it to a specific database, you use a Driver class
16
+ which encapsulates your program's interaction with the database behind a useful
17
+ interface. Now you can create a concrete implementation of the Driver class for
18
+ each kind of database you wish to talk to. If you make the base Driver class a
19
+ Pluggability, too, you can add new drivers simply by dropping them in a
20
+ directory and using the Driver's `create` method to instantiate them:
21
+
22
+ === Synopsis
23
+
24
+ in driver.rb:
25
+
26
+ require "Pluggability"
27
+
28
+ class Driver
29
+ include Pluggability
30
+ def self::derivative_dirs
31
+ ["drivers"]
32
+ end
33
+ end
34
+
35
+ in drivers/mysql.rb:
36
+
37
+ require 'driver'
38
+
39
+ class MysqlDriver < Driver
40
+ ...implementation...
41
+ end
42
+
43
+ in /usr/lib/ruby/1.8/PostgresDriver.rb:
44
+
45
+ require 'driver'
46
+
47
+ class PostgresDriver < Driver
48
+ ...implementation...
49
+ end
50
+
51
+ elsewhere
52
+
53
+ require 'driver'
54
+
55
+ config[:driver_type] #=> "mysql"
56
+ driver = Driver.create( config[:driver_type] )
57
+ driver.class #=> MysqlDriver
58
+ pgdriver = Driver.create( "PostGresDriver" )
59
+
60
+ === How Plugins Are Loaded
61
+
62
+ The +create+ class method added to your class by Pluggability searches for your
63
+ module using several different strategies. It tries various permutations of the
64
+ base class's name in combination with the derivative requested. For example,
65
+ assume we want to make a +DataDriver+ base class, and then use plugins to define
66
+ drivers for different kinds of data sources:
67
+
68
+ require 'pluggability'
69
+
70
+ class DataDriver
71
+ include Pluggability
72
+ end
73
+
74
+ When you attempt to load the 'socket' data-driver class like so:
75
+
76
+ DataDriver.create( 'socket' )
77
+
78
+ Pluggability searches for modules with the following names:
79
+
80
+ 'socketdatadriver'
81
+ 'socket_datadriver'
82
+ 'socketDataDriver'
83
+ 'socket_DataDriver'
84
+ 'SocketDataDriver'
85
+ 'Socket_DataDriver'
86
+ 'socket'
87
+ 'Socket'
88
+
89
+ Obviously the last one will load something other than what is intended, so you
90
+ can also tell Pluggability that plugins should be loaded from a subdirectory by
91
+ declaring a class method called `derivative_dirs` in the base class. It should
92
+ return an Array that contains a list of subdirectories to try:
93
+
94
+ class DataDriver
95
+ include Pluggability
96
+
97
+ def self::derivative_dirs
98
+ ['drivers']
99
+ end
100
+ end
101
+
102
+ This will change the list that is required to:
103
+
104
+ 'drivers/socketdatadriver'
105
+ 'drivers/socket_datadriver'
106
+ 'drivers/socketDataDriver'
107
+ 'drivers/socket_DataDriver'
108
+ 'drivers/SocketDataDriver'
109
+ 'drivers/Socket_DataDriver'
110
+ 'drivers/socket'
111
+ 'drivers/Socket'
112
+
113
+ If you return more than one subdirectory, each of them will be tried in turn:
114
+
115
+ class DataDriver
116
+ include Pluggability
117
+
118
+ def self::derivative_dirs
119
+ ['drivers', 'datadriver']
120
+ end
121
+ end
122
+
123
+ will change the search to include:
124
+
125
+ 'drivers/socketdatadriver'
126
+ 'drivers/socket_datadriver'
127
+ 'drivers/socketDataDriver'
128
+ 'drivers/socket_DataDriver'
129
+ 'drivers/SocketDataDriver'
130
+ 'drivers/Socket_DataDriver'
131
+ 'drivers/socket'
132
+ 'drivers/Socket'
133
+ 'datadriver/socketdatadriver'
134
+ 'datadriver/socket_datadriver'
135
+ 'datadriver/socketDataDriver'
136
+ 'datadriver/socket_DataDriver'
137
+ 'datadriver/SocketDataDriver'
138
+ 'datadriver/Socket_DataDriver'
139
+ 'datadriver/socket'
140
+ 'datadriver/Socket'
141
+
142
+ If the plugin is not found, a FactoryError is raised, and the message will list
143
+ all the permutations that were tried.
144
+
145
+ === Logging
146
+
147
+ If you need a little more insight into what's going on, Pluggability uses
148
+ 'Logger' from the standard library. Just set its logger to your own to include
149
+ log messages about plugins being loaded:
150
+
151
+
152
+ require 'pluggability'
153
+ require 'logger'
154
+
155
+ class DataDriver
156
+ include Pluggability
157
+
158
+ end
159
+
160
+ $logger = Logger.new( $stderr )
161
+ $logger.level = Logger::DEBUG
162
+ Pluggability.logger = $logger
163
+
164
+ DataDriver.create( 'ringbuffer' )
165
+
166
+ this might generate a log that looks like:
167
+
168
+ D, [...] DEBUG -- : Loading derivative ringbuffer
169
+ D, [...] DEBUG -- : Subdirs are: [""]
170
+ D, [...] DEBUG -- : Path is: ["ringbufferdatadriver", "ringbufferDataDriver",
171
+ "ringbuffer"]...
172
+ D, [...] DEBUG -- : Trying ringbufferdatadriver...
173
+ D, [...] DEBUG -- : No module at 'ringbufferdatadriver', trying the next
174
+ alternative: 'no such file to load -- ringbufferdatadriver'
175
+ D, [...] DEBUG -- : Trying ringbufferDataDriver...
176
+ D, [...] DEBUG -- : No module at 'ringbufferDataDriver', trying the next
177
+ alternative: 'no such file to load -- ringbufferDataDriver'
178
+ D, [...] DEBUG -- : Trying ringbuffer...
179
+ D, [...] DEBUG -- : No module at 'ringbuffer', trying the next alternative:
180
+ 'no such file to load -- ringbuffer'
181
+ D, [...] DEBUG -- : fatals = []
182
+ E, [...] ERROR -- : Couldn't find a DataDriver named 'ringbuffer':
183
+ tried ["ringbufferdatadriver", "ringbufferDataDriver", "ringbuffer"]
184
+
185
+
186
+
187
+ == Installation
188
+
189
+ gem install pluggability
190
+
191
+
192
+ == Contributing
193
+
194
+ You can check out the current development source with Mercurial via its
195
+ {Mercurial repo}[http://repo.deveiate.org/Pluggability]. Or if you prefer
196
+ Git, via {its Github mirror}[https://github.com/ged/pluggability].
197
+
198
+ After checking out the source, run:
199
+
200
+ $ rake newb
201
+
202
+ This task will install any missing dependencies, run the tests/specs,
203
+ and generate the API documentation.
204
+
205
+
206
+ == License
207
+
208
+ Copyright (c) 2008-2012, Michael Granger and Martin Chase
209
+ All rights reserved.
210
+
211
+ Redistribution and use in source and binary forms, with or without
212
+ modification, are permitted provided that the following conditions are met:
213
+
214
+ * Redistributions of source code must retain the above copyright notice,
215
+ this list of conditions and the following disclaimer.
216
+
217
+ * Redistributions in binary form must reproduce the above copyright notice,
218
+ this list of conditions and the following disclaimer in the documentation
219
+ and/or other materials provided with the distribution.
220
+
221
+ * Neither the name of the author/s, nor the names of the project's
222
+ contributors may be used to endorse or promote products derived from this
223
+ software without specific prior written permission.
224
+
225
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
226
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
227
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
228
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
229
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
230
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
231
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
232
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
233
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
234
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
235
+
236
+
237
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'hoe'
4
+
5
+ Hoe.plugin :deveiate
6
+ Hoe.plugin :mercurial
7
+ Hoe.plugin :signing
8
+
9
+ Hoe.plugins.delete :rubyforge
10
+
11
+ hoespec = Hoe.spec 'pluggability' do
12
+ self.readme_file = 'README.rdoc'
13
+ self.history_file = 'History.rdoc'
14
+ self.extra_rdoc_files = Rake::FileList[ '*.rdoc' ]
15
+ self.spec_extras[:rdoc_options] = ['-f', 'fivefish', '-t', 'Pluggability Toolkit']
16
+
17
+ self.developer 'Martin Chase', 'stillflame@FaerieMUD.org'
18
+ self.developer 'Michael Granger', 'ged@FaerieMUD.org'
19
+
20
+ self.dependency 'loggability', '~> 0.5'
21
+
22
+ self.dependency 'hoe-deveiate', '~> 0.1', :development
23
+
24
+ self.spec_extras[:licenses] = ["BSD"]
25
+ self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
26
+ end
27
+
28
+ ENV['VERSION'] ||= hoespec.spec.version.to_s
29
+
30
+ task 'hg:precheckin' => [ :check_history, :check_manifest, :spec ]
31
+
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'loggability' unless defined?( Loggability )
4
+
5
+
6
+ # The Pluggability module
7
+ module Pluggability
8
+ extend Loggability
9
+
10
+ # Loggability API -- Set up a logger.
11
+ log_as :pluggability
12
+
13
+
14
+ # Library version
15
+ VERSION = '0.0.1'
16
+
17
+
18
+ ### An exception class for Pluggability specific errors.
19
+ class FactoryError < RuntimeError; end
20
+
21
+
22
+ ### Add the @derivatives instance variable to including classes.
23
+ def self::extend_object( obj )
24
+ obj.instance_variable_set( :@plugin_prefixes, [] )
25
+ obj.instance_variable_set( :@derivatives, {} )
26
+ super
27
+ end
28
+
29
+
30
+ #############################################################
31
+ ### M I X I N M E T H O D S
32
+ #############################################################
33
+
34
+ ### Get/set the prefixes that will be used when searching for particular plugins
35
+ ### for the calling Class.
36
+ def plugin_prefixes( *args )
37
+ @plugin_prefixes.replace( args ) if !args.empty?
38
+ return @plugin_prefixes
39
+ end
40
+
41
+
42
+ ### Set the prefixes that will be used when searching for particular plugins
43
+ ### for the calling Class.
44
+ def plugin_prefixes=( args )
45
+ @plugin_prefixes = Array( args )
46
+ end
47
+
48
+
49
+ ### Return the Hash of derivative classes, keyed by various versions of
50
+ ### the class name.
51
+ def derivatives
52
+ ancestors.each do |klass|
53
+ if klass.instance_variables.include?( :@derivatives ) ||
54
+ klass.instance_variables.include?( "@derivatives" )
55
+ return klass.instance_variable_get( :@derivatives )
56
+ end
57
+ end
58
+ end
59
+
60
+
61
+ ### Returns the type name used when searching for a derivative.
62
+ def factory_type
63
+ base = nil
64
+ self.ancestors.each do |klass|
65
+ if klass.instance_variables.include?( :@derivatives ) ||
66
+ klass.instance_variables.include?( "@derivatives" )
67
+ base = klass
68
+ break
69
+ end
70
+ end
71
+
72
+ raise FactoryError, "Couldn't find factory base for #{self.name}" if
73
+ base.nil?
74
+
75
+ if base.name =~ /^.*::(.*)/
76
+ return $1
77
+ else
78
+ return base.name
79
+ end
80
+ end
81
+
82
+
83
+ ### Inheritance callback -- Register subclasses in the derivatives hash
84
+ ### so that ::create knows about them.
85
+ def inherited( subclass )
86
+ keys = [ subclass ]
87
+
88
+ # If it's not an anonymous class, make some keys out of variants of its name
89
+ if subclass.name
90
+ simple_name = subclass.name.sub( /#<Class:0x[[:xdigit:]]+>::/i, '' )
91
+ keys << simple_name << simple_name.downcase
92
+
93
+ # Handle class names like 'FooBar' for 'Bar' factories.
94
+ Pluggability.log.debug "Inherited %p for %p-type plugins" % [ subclass, self.factory_type ]
95
+ if subclass.name.match( /(?:.*::)?(\w+)(?:#{self.factory_type})/i )
96
+ keys << Regexp.last_match[1].downcase
97
+ else
98
+ keys << subclass.name.sub( /.*::/, '' ).downcase
99
+ end
100
+ else
101
+ Pluggability.log.debug " no name-based variants for anonymous subclass %p" % [ subclass ]
102
+ end
103
+
104
+ keys.compact.uniq.each do |key|
105
+ Pluggability.log.info "Registering %s derivative of %s as %p" %
106
+ [ subclass.name, self.name, key ]
107
+ self.derivatives[ key ] = subclass
108
+ end
109
+
110
+ super
111
+ end
112
+
113
+
114
+ ### Returns an Array of registered derivatives
115
+ def derivative_classes
116
+ self.derivatives.values.uniq
117
+ end
118
+
119
+
120
+ ### Given the <tt>class_name</tt> of the class to instantiate, and other
121
+ ### arguments bound for the constructor of the new object, this method
122
+ ### loads the derivative class if it is not loaded already (raising a
123
+ ### LoadError if an appropriately-named file cannot be found), and
124
+ ### instantiates it with the given <tt>args</tt>. The <tt>class_name</tt>
125
+ ### may be the the fully qualified name of the class, the class object
126
+ ### itself, or the unique part of the class name. The following examples
127
+ ### would all try to load and instantiate a class called "FooListener"
128
+ ### if Listener included Factory
129
+ ### obj = Listener.create( 'FooListener' )
130
+ ### obj = Listener.create( FooListener )
131
+ ### obj = Listener.create( 'Foo' )
132
+ def create( class_name, *args, &block )
133
+ subclass = get_subclass( class_name )
134
+
135
+ begin
136
+ return subclass.new( *args, &block )
137
+ rescue => err
138
+ nicetrace = err.backtrace.reject {|frame| /#{__FILE__}/ =~ frame}
139
+ msg = "When creating '#{class_name}': " + err.message
140
+ Kernel.raise( err, msg, nicetrace )
141
+ end
142
+ end
143
+
144
+
145
+ ### Given a <tt>class_name</tt> like that of the first argument to
146
+ ### #create, attempt to load the corresponding class if it is not
147
+ ### already loaded and return the class object.
148
+ def get_subclass( class_name )
149
+ return self if ( self.name == class_name || class_name == '' )
150
+ if class_name.is_a?( Class )
151
+ return class_name if class_name <= self
152
+ raise ArgumentError, "%s is not a descendent of %s" % [class_name, self]
153
+ end
154
+
155
+ class_name = class_name.to_s
156
+
157
+ # If the derivatives hash doesn't already contain the class, try to load it
158
+ unless self.derivatives.has_key?( class_name.downcase )
159
+ self.load_derivative( class_name )
160
+
161
+ subclass = self.derivatives[ class_name.downcase ]
162
+ unless subclass.is_a?( Class )
163
+ raise FactoryError,
164
+ "load_derivative(%s) added something other than a class "\
165
+ "to the registry for %s: %p" %
166
+ [ class_name, self.name, subclass ]
167
+ end
168
+ end
169
+
170
+ return self.derivatives[ class_name.downcase ]
171
+ end
172
+
173
+
174
+ ### Find and load all derivatives of this class, using plugin_prefixes if any
175
+ ### are defined, or a pattern derived from the #factory_type if not. Returns
176
+ ### an array of all derivative classes. Load failures are logged but otherwise
177
+ ### ignored.
178
+ def load_all
179
+ patterns = []
180
+ prefixes = self.plugin_prefixes
181
+
182
+ if prefixes && !prefixes.empty?
183
+ Pluggability.log.debug "Using plugin prefixes (%p) to build load patterns." % [ prefixes ]
184
+ prefixes.each do |prefix|
185
+ patterns << "#{prefix}/*.rb"
186
+ end
187
+ else
188
+ # Use all but the last pattern, which will just be '*.rb'
189
+ Pluggability.log.debug "Using factory type (%p) to build load patterns." %
190
+ [ self.factory_type ]
191
+ patterns += self.make_require_path( '*', '' )[0..-2].
192
+ map {|f| f + '.rb' }
193
+ end
194
+
195
+ patterns.each do |glob|
196
+ Pluggability.log.debug " finding derivatives matching pattern %p" % [ glob ]
197
+ candidates = Gem.find_files( glob )
198
+ Pluggability.log.debug " found %d matching files" % [ candidates.length ]
199
+ next if candidates.empty?
200
+
201
+ candidates.each {|path| require(path) }
202
+ end
203
+
204
+ return self.derivative_classes
205
+ end
206
+
207
+
208
+ ### Calculates an appropriate filename for the derived class using the
209
+ ### name of the base class and tries to load it via <tt>require</tt>. If
210
+ ### the including class responds to a method named
211
+ ### <tt>derivativeDirs</tt>, its return value (either a String, or an
212
+ ### array of Strings) is added to the list of prefix directories to try
213
+ ### when attempting to require a modules. Eg., if
214
+ ### <tt>class.derivativeDirs</tt> returns <tt>['foo','bar']</tt> the
215
+ ### require line is tried with both <tt>'foo/'</tt> and <tt>'bar/'</tt>
216
+ ### prepended to it.
217
+ def load_derivative( class_name )
218
+ Pluggability.log.debug "Loading derivative #{class_name}"
219
+
220
+ # Get the unique part of the derived class name and try to
221
+ # load it from one of the derivative subdirs, if there are
222
+ # any.
223
+ mod_name = self.get_module_name( class_name )
224
+ result = self.require_derivative( mod_name )
225
+
226
+ # Check to see if the specified listener is now loaded. If it
227
+ # is not, raise an error to that effect.
228
+ unless self.derivatives[ class_name.downcase ]
229
+ errmsg = "Require of '%s' succeeded, but didn't load a %s named '%s' for some reason." % [
230
+ result,
231
+ self.factory_type,
232
+ class_name.downcase,
233
+ ]
234
+ Pluggability.log.error( errmsg )
235
+ raise FactoryError, errmsg, caller(3)
236
+ end
237
+ end
238
+
239
+
240
+ ### Build and return the unique part of the given <tt>class_name</tt>
241
+ ### either by stripping leading namespaces if the name already has the
242
+ ### name of the factory type in it (eg., 'My::FooService' for Service,
243
+ ### or by appending the factory type if it doesn't.
244
+ def get_module_name( class_name )
245
+ if class_name =~ /\w+#{self.factory_type}/
246
+ mod_name = class_name.sub( /(?:.*::)?(\w+)(?:#{self.factory_type})/, "\\1" )
247
+ else
248
+ mod_name = class_name
249
+ end
250
+
251
+ return mod_name
252
+ end
253
+
254
+
255
+ ### Search for the module with the specified <tt>mod_name</tt>, using any
256
+ ### #plugin_prefixes that have been set.
257
+ def require_derivative( mod_name )
258
+
259
+ subdirs = self.plugin_prefixes
260
+ subdirs << '' if subdirs.empty?
261
+ Pluggability.log.debug "Subdirs are: %p" % [subdirs]
262
+ fatals = []
263
+ tries = []
264
+
265
+ # Iterate over the subdirs until we successfully require a
266
+ # module.
267
+ subdirs.map( &:strip ).each do |subdir|
268
+ self.make_require_path( mod_name, subdir ).each do |path|
269
+ Pluggability.logger.debug "Trying #{path}..."
270
+ tries << path
271
+
272
+ # Try to require the module, saving errors and jumping
273
+ # out of the catch block on success.
274
+ begin
275
+ require( path.untaint )
276
+ rescue LoadError => err
277
+ Pluggability.log.debug "No module at '%s', trying the next alternative: '%s'" %
278
+ [ path, err.message ]
279
+ rescue Exception => err
280
+ fatals << err
281
+ Pluggability.log.error "Found '#{path}', but encountered an error: %s\n\t%s" %
282
+ [ err.message, err.backtrace.join("\n\t") ]
283
+ else
284
+ Pluggability.log.info "Loaded '#{path}' without error."
285
+ return path
286
+ end
287
+ end
288
+ end
289
+
290
+ Pluggability.logger.debug "fatals = %p" % [ fatals ]
291
+
292
+ # Re-raise is there was a file found, but it didn't load for
293
+ # some reason.
294
+ if fatals.empty?
295
+ errmsg = "Couldn't find a %s named '%s': tried %p" % [
296
+ self.factory_type,
297
+ mod_name,
298
+ tries
299
+ ]
300
+ Pluggability.log.error( errmsg )
301
+ raise FactoryError, errmsg
302
+ else
303
+ Pluggability.log.debug "Re-raising first fatal error"
304
+ Kernel.raise( fatals.first )
305
+ end
306
+ end
307
+
308
+
309
+ ### Make a list of permutations of the given +modname+ for the given
310
+ ### +subdir+. Called on a +DataDriver+ class with the arguments 'Socket' and
311
+ ### 'drivers', returns:
312
+ ### ["drivers/socketdatadriver", "drivers/socketDataDriver",
313
+ ### "drivers/SocketDataDriver", "drivers/socket", "drivers/Socket"]
314
+ def make_require_path( modname, subdir )
315
+ path = []
316
+ myname = self.factory_type
317
+
318
+ # Make permutations of the two parts
319
+ path << modname
320
+ path << modname.downcase
321
+ path << modname + myname
322
+ path << modname.downcase + myname
323
+ path << modname.downcase + myname.downcase
324
+ path << modname + '_' + myname
325
+ path << modname.downcase + '_' + myname
326
+ path << modname.downcase + '_' + myname.downcase
327
+
328
+ # If a non-empty subdir was given, prepend it to all the items in the
329
+ # path
330
+ unless subdir.nil? or subdir.empty?
331
+ path.collect! {|m| File.join(subdir, m)}
332
+ end
333
+
334
+ Pluggability.log.debug "Path is: #{path.uniq.reverse.inspect}..."
335
+ return path.uniq.reverse
336
+ end
337
+
338
+ end # module Pluggability
339
+
340
+
341
+ # Backward-compatibility alias
342
+ FactoryError = Pluggability::FactoryError unless defined?( FactoryError )
343
+