Chris@909: module CodeRay Chris@909: Chris@909: # = PluginHost Chris@909: # Chris@909: # A simple subclass/subfolder plugin system. Chris@909: # Chris@909: # Example: Chris@909: # class Generators Chris@909: # extend PluginHost Chris@909: # plugin_path 'app/generators' Chris@909: # end Chris@909: # Chris@909: # class Generator Chris@909: # extend Plugin Chris@909: # PLUGIN_HOST = Generators Chris@909: # end Chris@909: # Chris@909: # class FancyGenerator < Generator Chris@909: # register_for :fancy Chris@909: # end Chris@909: # Chris@909: # Generators[:fancy] #-> FancyGenerator Chris@909: # # or Chris@909: # CodeRay.require_plugin 'Generators/fancy' Chris@909: # # or Chris@909: # Generators::Fancy Chris@909: module PluginHost Chris@909: Chris@909: # Raised if Encoders::[] fails because: Chris@909: # * a file could not be found Chris@909: # * the requested Plugin is not registered Chris@909: PluginNotFound = Class.new LoadError Chris@909: HostNotFound = Class.new LoadError Chris@909: Chris@909: PLUGIN_HOSTS = [] Chris@909: PLUGIN_HOSTS_BY_ID = {} # dummy hash Chris@909: Chris@909: # Loads all plugins using list and load. Chris@909: def load_all Chris@909: for plugin in list Chris@909: load plugin Chris@909: end Chris@909: end Chris@909: Chris@909: # Returns the Plugin for +id+. Chris@909: # Chris@909: # Example: Chris@909: # yaml_plugin = MyPluginHost[:yaml] Chris@909: def [] id, *args, &blk Chris@909: plugin = validate_id(id) Chris@909: begin Chris@909: plugin = plugin_hash.[] plugin, *args, &blk Chris@909: end while plugin.is_a? Symbol Chris@909: plugin Chris@909: end Chris@909: Chris@909: alias load [] Chris@909: Chris@909: # Tries to +load+ the missing plugin by translating +const+ to the Chris@909: # underscore form (eg. LinesOfCode becomes lines_of_code). Chris@909: def const_missing const Chris@909: id = const.to_s. Chris@909: gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). Chris@909: gsub(/([a-z\d])([A-Z])/,'\1_\2'). Chris@909: downcase Chris@909: load id Chris@909: end Chris@909: Chris@909: class << self Chris@909: Chris@909: # Adds the module/class to the PLUGIN_HOSTS list. Chris@909: def extended mod Chris@909: PLUGIN_HOSTS << mod Chris@909: end Chris@909: Chris@909: end Chris@909: Chris@909: # The path where the plugins can be found. Chris@909: def plugin_path *args Chris@909: unless args.empty? Chris@909: @plugin_path = File.expand_path File.join(*args) Chris@909: end Chris@909: @plugin_path ||= '' Chris@909: end Chris@909: Chris@909: # Map a plugin_id to another. Chris@909: # Chris@909: # Usage: Put this in a file plugin_path/_map.rb. Chris@909: # Chris@909: # class MyColorHost < PluginHost Chris@909: # map :navy => :dark_blue, Chris@909: # :maroon => :brown, Chris@909: # :luna => :moon Chris@909: # end Chris@909: def map hash Chris@909: for from, to in hash Chris@909: from = validate_id from Chris@909: to = validate_id to Chris@909: plugin_hash[from] = to unless plugin_hash.has_key? from Chris@909: end Chris@909: end Chris@909: Chris@909: # Define the default plugin to use when no plugin is found Chris@909: # for a given id, or return the default plugin. Chris@909: # Chris@909: # See also map. Chris@909: # Chris@909: # class MyColorHost < PluginHost Chris@909: # map :navy => :dark_blue Chris@909: # default :gray Chris@909: # end Chris@909: # Chris@909: # MyColorHost.default # loads and returns the Gray plugin Chris@909: def default id = nil Chris@909: if id Chris@909: id = validate_id id Chris@909: raise "The default plugin can't be named \"default\"." if id == :default Chris@909: plugin_hash[:default] = id Chris@909: else Chris@909: load :default Chris@909: end Chris@909: end Chris@909: Chris@909: # Every plugin must register itself for +id+ by calling register_for, Chris@909: # which calls this method. Chris@909: # Chris@909: # See Plugin#register_for. Chris@909: def register plugin, id Chris@909: plugin_hash[validate_id(id)] = plugin Chris@909: end Chris@909: Chris@909: # A Hash of plugion_id => Plugin pairs. Chris@909: def plugin_hash Chris@909: @plugin_hash ||= make_plugin_hash Chris@909: end Chris@909: Chris@909: # Returns an array of all .rb files in the plugin path. Chris@909: # Chris@909: # The extension .rb is not included. Chris@909: def list Chris@909: Dir[path_to('*')].select do |file| Chris@909: File.basename(file)[/^(?!_)\w+\.rb$/] Chris@909: end.map do |file| Chris@909: File.basename(file, '.rb').to_sym Chris@909: end Chris@909: end Chris@909: Chris@909: # Returns an array of all Plugins. Chris@909: # Chris@909: # Note: This loads all plugins using load_all. Chris@909: def all_plugins Chris@909: load_all Chris@909: plugin_hash.values.grep(Class) Chris@909: end Chris@909: Chris@909: # Loads the map file (see map). Chris@909: # Chris@909: # This is done automatically when plugin_path is called. Chris@909: def load_plugin_map Chris@909: mapfile = path_to '_map' Chris@909: @plugin_map_loaded = true Chris@909: if File.exist? mapfile Chris@909: require mapfile Chris@909: true Chris@909: else Chris@909: false Chris@909: end Chris@909: end Chris@909: Chris@909: protected Chris@909: Chris@909: # Return a plugin hash that automatically loads plugins. Chris@909: def make_plugin_hash Chris@909: @plugin_map_loaded ||= false Chris@909: Hash.new do |h, plugin_id| Chris@909: id = validate_id(plugin_id) Chris@909: path = path_to id Chris@909: begin Chris@909: raise LoadError, "#{path} not found" unless File.exist? path Chris@909: require path Chris@909: rescue LoadError => boom Chris@909: if @plugin_map_loaded Chris@909: if h.has_key?(:default) Chris@909: warn '%p could not load plugin %p; falling back to %p' % [self, id, h[:default]] Chris@909: h[:default] Chris@909: else Chris@909: raise PluginNotFound, '%p could not load plugin %p: %s' % [self, id, boom] Chris@909: end Chris@909: else Chris@909: load_plugin_map Chris@909: h[plugin_id] Chris@909: end Chris@909: else Chris@909: # Plugin should have registered by now Chris@909: if h.has_key? id Chris@909: h[id] Chris@909: else Chris@909: raise PluginNotFound, "No #{self.name} plugin for #{id.inspect} found in #{path}." Chris@909: end Chris@909: end Chris@909: end Chris@909: end Chris@909: Chris@909: # Returns the expected path to the plugin file for the given id. Chris@909: def path_to plugin_id Chris@909: File.join plugin_path, "#{plugin_id}.rb" Chris@909: end Chris@909: Chris@909: # Converts +id+ to a Symbol if it is a String, Chris@909: # or returns +id+ if it already is a Symbol. Chris@909: # Chris@909: # Raises +ArgumentError+ for all other objects, or if the Chris@909: # given String includes non-alphanumeric characters (\W). Chris@909: def validate_id id Chris@909: if id.is_a? Symbol or id.nil? Chris@909: id Chris@909: elsif id.is_a? String Chris@909: if id[/\w+/] == id Chris@909: id.downcase.to_sym Chris@909: else Chris@909: raise ArgumentError, "Invalid id given: #{id}" Chris@909: end Chris@909: else Chris@909: raise ArgumentError, "String or Symbol expected, but #{id.class} given." Chris@909: end Chris@909: end Chris@909: Chris@909: end Chris@909: Chris@909: Chris@909: # = Plugin Chris@909: # Chris@909: # Plugins have to include this module. Chris@909: # Chris@909: # IMPORTANT: Use extend for this module. Chris@909: # Chris@909: # See CodeRay::PluginHost for examples. Chris@909: module Plugin Chris@909: Chris@909: attr_reader :plugin_id Chris@909: Chris@909: # Register this class for the given +id+. Chris@909: # Chris@909: # Example: Chris@909: # class MyPlugin < PluginHost::BaseClass Chris@909: # register_for :my_id Chris@909: # ... Chris@909: # end Chris@909: # Chris@909: # See PluginHost.register. Chris@909: def register_for id Chris@909: @plugin_id = id Chris@909: plugin_host.register self, id Chris@909: end Chris@909: Chris@909: # Returns the title of the plugin, or sets it to the Chris@909: # optional argument +title+. Chris@909: def title title = nil Chris@909: if title Chris@909: @title = title.to_s Chris@909: else Chris@909: @title ||= name[/([^:]+)$/, 1] Chris@909: end Chris@909: end Chris@909: Chris@909: # The PluginHost for this Plugin class. Chris@909: def plugin_host host = nil Chris@909: if host.is_a? PluginHost Chris@909: const_set :PLUGIN_HOST, host Chris@909: end Chris@909: self::PLUGIN_HOST Chris@909: end Chris@909: Chris@909: def aliases Chris@909: plugin_host.load_plugin_map Chris@909: plugin_host.plugin_hash.inject [] do |aliases, (key, _)| Chris@909: aliases << key if plugin_host[key] == self Chris@909: aliases Chris@909: end Chris@909: end Chris@909: Chris@909: end Chris@909: Chris@909: end