
# == Synopsis
# LittlePlugger is a module that provides Gem based plugin management.
# By extending your own class or module with LittlePlugger you can easily
# manage the loading and initializing of plugins provided by other gems.
#
# == Details
# Plugins are great! They allow other developers to add functionality to
# an application but relieve the application developer of the responsibility
# for mainting some other developer's plugin code. LittlePlugger aims to
# make it dead simple to manage external plugins as gems.
#
# === Naming
# Every plugin managed by LittlePlugger will have a name represented as a
# Symbol. This name is used to register the plugin, load the plugin file,
# and manage the plugin class/module. Here are the three rules for plugin
# names:
#
# 1) all lowercase with underscores
# 2) maps to a file of the same name with an '.rb' extension
# 3) converting the name to camel case yields the plugin class / module
#
# These rules are essentially the standard ruby practice of naming files
# after the class / module the file defines.
#
# === Finding & Loading
# Plugins are found by searching through the lib folders of all installed
# gems; these gems are not necessarily loaded - just searched. If the lib
# folder has a subdirectory that matches the +plugin_path+, then all ruby
# files in the gem's +plugin_path+ are noted for later loading.
#
# A file is only loaded if the basename of the file matches one of the
# registered plugin names. If no plugins are registered, then every file in
# the +plugin_path+ is loaded.
#
# The plugin classes / modules are all expected to live in the same
# namespace for a particular application. For example, all plugins for the
# "Foo" application should reside in a "Foo::Plugins" namespace. This allows
# the plugins to be automatically initialized by LittlePlugger.
#
# === Initializing
# Optionally, plugins can provide an initialization method for running any
# setup code needed by the plugin. This initialize method should be named as
# follows: "initializer_#{plugin_name}" where the name of the plugin is
# appended to the end of the initializer method name.
#
# If this method exists, it will be called automatically when plugins are
# loaded. The order of loading of initialization is not strictly defined, so
# do not rely on another plugin being initialized for your own plugin
# successfully initialize.
#
# == Usage
# LittlePlugger is used by extending your own class or module with the
# LittlePlugger module.
#
#    module Logging
#      extend LittlePlugger
#    end
#
# This defines a +plugin_path+ and a +plugin_module+ for our Logging module.
# The +plugin_path+ is set to "logging/plugins", and therefore, the
# +plugin_modlue+ is defined as Logging::Plugins. All plugins for the
# Logging module should be found underneath this plugin module.
#
# The plugins for the Logging module are loaded and initialized by calling
# the +initialize_plugins+ method.
#
#    Logging.initialize_plugins
#
# If you only want to load the plugin files but not initialize the plugin
# classes / modules then you can call the +load_plugins+ method.
#
#    Logging.load_plugins
#
# Finally, you can get a hash of all the loaded plugins.
#
#    Logging.plugins
#
# This returns a hash keyed by the plugin names with the plugin class /
# module as the value.
#
# If you only want a certain set of plugins to be loaded, then pass the
# names to the +plugin+ method.
#
#    Logging.plugin :foo, :bar, :baz
#
# Now only three plugins for the Logging module will be loaded.
#
# === Customizing
# LittlePlugger allows the use of a custom plugin path and module. These are
# specified when extending with LilttlePlugger by passing the specific path
# and module to LittlePlugger.
#
#    class Hoe
#      extend LittlePlugger( :path => 'hoe', :module => Hoe )
#
#      plugin(
#          :clean, :debug, :deps, :flay, :flog, :package,
#          :publish, :rcov, :signing, :test
#      )
#    end
#
# All ruby files found under the "hoe" directory will be treated as
# plugins, and the plugin classes / modules should reside directly under the
# Hoe namespace.
#
# We also specify a list of plugins to be loaded. Only these plugins will be
# loaded and initialized by the LittlePlugger module. The +plugin+ method
# can be called multiple times to add more plugins.
#
module LittlePlugger

  VERSION = '1.1.3'  # :nodoc:

  # Returns the version string for the library.
  #
  def self.version
    VERSION
  end

  module ClassMethods

    # Add the _names_ to the list of plugins that will be loaded.
    #
    def plugin( *names )
      plugin_names.concat(names.map! {|n| n.to_sym})
    end

    # Add the _names_ to the list of plugins that will *not* be loaded. This
    # list prevents the plugin system from loading unwanted or unneeded
    # plugins.
    #
    # If a plugin name appears in both the 'disregard_plugin' list and the
    # 'plugin' list, the disregard list takes precedence; that is, the plugin
    # will not be loaded.
    #
    def disregard_plugin( *names )
      @disregard_plugin ||= []
      @disregard_plugin.concat(names.map! {|n| n.to_sym})
      @disregard_plugin
    end
    alias :disregard_plugins :disregard_plugin

    # Returns the array of plugin names that will be loaded. If the array is
    # empty, then any plugin found in the +plugin_path+ will be loaded.
    #
    def plugin_names
      @plugin_names ||= []
    end

    # Loads the desired plugins and returns a hash. The hash contains all
    # the plugin classes and modules keyed by the plugin name.
    #
    def plugins
      load_plugins
      pm = plugin_module
      names = pm.constants.map { |s| s.to_s }
      names.reject! { |n| n =~ %r/^[A-Z_]+$/ }

      h = {}
      names.each do |name|
        sym = ::LittlePlugger.underscore(name).to_sym
        next unless plugin_names.empty? or plugin_names.include? sym
        next if disregard_plugins.include? sym
        h[sym] = pm.const_get name
      end
      h
    end

    # Iterate over the loaded plugin classes and modules and call the
    # initialize method for each plugin. The plugin's initialize method is
    # defeind as +initialize_plugin_name+, where the plugin name is unique
    # to each plugin.
    #
    def initialize_plugins
      plugins.each do |name, klass|
        msg = "initialize_#{name}"
        klass.send msg if klass.respond_to? msg
      end
    end

    # Iterate through all installed gems looking for those that have the
    # +plugin_path+ in their "lib" folder, and load all .rb files found in
    # the gem's plugin path. Each .rb file should define one class or module
    # that will be used as a plugin.
    #
    def load_plugins
      @loaded ||= {}
      found = {}

      Gem.find_files(File.join(plugin_path, '*.rb')).sort!.reverse_each do |path|
        name = File.basename(path, '.rb').to_sym
        found[name] = path unless found.key? name
      end

      :keep_on_truckin while found.map { |name, path|
        next unless plugin_names.empty? or plugin_names.include? name
        next if disregard_plugins.include? name
        next if @loaded[name]
        begin
          @loaded[name] = load path
        rescue ScriptError, StandardError => err
          warn "Error loading #{path.inspect}: #{err.message}. skipping..."
        end
      }.any?
    end

    # The path to search in a gem's 'lib' folder for plugins.
    #
    def plugin_path
      ::LittlePlugger.default_plugin_path(self)
    end

    # This module or class where plugins are located.
    #
    def plugin_module
      ::LittlePlugger.default_plugin_module(plugin_path)
    end

  end  # module ClassMethods

  # :stopdoc:

  # Called when another object extends itself with LittlePlugger.
  #
  def self.extended( other )
    other.extend ClassMethods
  end

  # Convert the given string from camel case to snake case. Method liberally
  # stolen from ActiveSupport.
  #
  #    underscore( "FooBar" )    #=> "foo_bar"
  #
  def self.underscore( string )
    string.to_s.
        gsub(%r/::/, '/').
        gsub(%r/([A-Z]+)([A-Z][a-z])/,'\1_\2').
        gsub(%r/([a-z\d])([A-Z])/,'\1_\2').
        tr('-', '_').
        downcase
  end

  # For a given object returns a default plugin path. The path is
  # created by splitting the object's class name on the namespace separator
  # "::" and converting each part of the namespace into an underscored
  # string (see the +underscore+ method). The strings are then joined using
  # the File#join method to give a filesystem path. Appended to this path is
  # the 'plugins' directory.
  #
  #    default_plugin_path( FooBar::Baz )    #=> "foo_bar/baz/plugins"
  #
  def self.default_plugin_path( obj )
    obj = obj.class unless obj.is_a? Module
    File.join(underscore(obj.name), 'plugins')
  end

  # For a given path returns the class or module corresponding to the
  # path. This method assumes a correspondence between directory names and
  # Ruby namespaces.
  #
  #    default_plugin_module( "foo_bar/baz/plugins" )  #=> FooBar::Baz::Plugins
  #
  # This method will fail if any of the namespaces have not yet been
  # defined.
  #
  def self.default_plugin_module( path )
    path.split(File::SEPARATOR).inject(Object) do |mod, const|
      const = const.split('_').map { |s| s.capitalize }.join
      mod.const_get const
    end
  end
  # :startdoc:

end  # module LittlePlugger


module Kernel

  # call-seq:
  #    LittlePlugger( opts = {} )
  #
  # This method allows the user to override some of LittlePlugger's default
  # settings when mixed into a module or class.
  #
  # See the "Customizing" section of the LittlePlugger documentation for an
  # example of how this method is used.
  #
  # ==== Options
  #
  # * :path <String>
  #    The default plugin path. Defaults to "module_name/plugins".
  #
  # * :module <Module>
  #    The module where plugins will be loaded. Defaults to
  #    ModuleName::Plugins.
  #
  # * :plugins <Array>
  #    The array of default plugins to load. Only the plugins listed in this
  #    array will be loaded by LittlePlugger.
  # 
  def LittlePlugger( opts = {} )
    return ::LittlePlugger::ClassMethods if opts.empty?
    Module.new {
      include ::LittlePlugger::ClassMethods

      if opts.key?(:path)
        eval %Q{def plugin_path() #{opts[:path].to_s.inspect} end}
      end

      if opts.key?(:module)
        eval %Q{def plugin_module() #{opts[:module].name} end}
      end

      if opts.key?(:plugins)
        plugins = Array(opts[:plugins]).map {|val| val.to_sym.inspect}.join(',')
        eval %Q{def plugin_names() @plugin_names ||= [#{plugins}] end}
      end
    }
  end
end  # module Kernel

# EOF
