Skip to content

Instantly share code, notes, and snippets.

@abargnesi
Last active November 4, 2015 19:57
Show Gist options
  • Save abargnesi/8c9e27620b8ce0369dcb to your computer and use it in GitHub Desktop.
Save abargnesi/8c9e27620b8ce0369dcb to your computer and use it in GitHub Desktop.
plugin proof of concept
module Hardwire::Plugins
module Bar
class BarPlugin
include Hardwire::PluginDefinition
def self.plugin_file_path
__FILE__
end
def identifiers
:bar
end
def name
:Bar
end
def create(options = {})
BarShazam.new
end
end
class BarShazam
def drink(number = 1)
number.times do
puts "gulp..."
sleep 3
end
end
end
end
end
module Hardwire::Plugins
module Foo
class FooPlugin
include Hardwire::PluginDefinition
def self.plugin_file_path
__FILE__
end
def identifiers
:foo
end
def name
:Foo
end
def create(options = {})
FooShazam.new
end
end
class FooShazam
def learn(skill)
puts "meditating..."
sleep 3
puts "* you know #{skill}"
end
end
end
end
# Usage
#
# Require:
# require './hardwire_vendored.rb'
# Load a Container given a base path found on $LOAD_PATH.
# Hardwire::Container.new('hardwire/examples')
# Retrieve all available plugins in Container.
# Hardwire::Container.new('hardwire/examples').plugins.to_a
# Use a plugin in a Container. This activates then creates the identified
# plugin.
# Hardwire::Container.new('hardwire/examples').use(:foo)
module Hardwire
VERSION = "0.1.0"
end
module Hardwire
module Plugins
# Single, global namespace where plugins are defined.
# Having a global namespace could cause overlap if multiple instances of
# Hardwire are used in the same process, but this wouldn't be a short-term
# problem worth accounting for now.
end
end
module Hardwire
# Defines a contract that a plugin instance should respond to. Methods that
# need to be overriden will throw {NotImplementedError}. Methods that are
# optional will be empty (i.e. no-op).
module PluginDefinition
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
module ClassMethods
def plugin_file_path
msg = %Q{
#{__method__} is not implemented.
The plugin_file_path method must be defined to return __FILE__
so that plugin containers know if its defined in *their* base path.
__FILE__ is lexically scoped so it must be defined within the file
defining the plugin definition (e.g. foo/def.rb).
}
raise NotImplementedError.new(msg)
end
end
module InstanceMethods
# Returns the identifiers that can be used to find this plugin.
def identifiers
raise NotImplementedError.new("#{__method__} is not implemented.")
end
# Returns the name of this plugin.
def name
raise NotImplementedError.new("#{__method__} is not implemented.")
end
# Returns the description of this plugin.
def description
end
# Activates this plugin for use. Any exceptions raised will result in a
# call to {#on_failed_activation}, passing the raised exception, to allow
# this plugin to provide assistance.
#
# Example usage:
#
# - require plugin code plus dependencies
# - initial configuration
# - test external connection to database
def activate
end
# Create this plugin for use and returns a plugin object for use.
def create(options = {})
raise NotImplementedError.new("#{__method__} is not implemented.")
end
# Called when this plugin has been successfully activated.
def on_successful_activation
end
# Called when this plugin has failed to activate. In this case an
# exception would have occurred.
def on_failed_activation(exception)
#TODO Can we reraise with #activate is on the top of the backtrace?
raise exception
end
end
end
end
require 'pathname'
require 'rubygems'
module Hardwire::Mixin
module Query
# Retrieves plugins that are available on the {$LOAD_PATH} given the
# {#plugin_base_path} in {Hardwire::Container}.
def plugins(*ids)
plugin_definition_paths.each do |path|
Kernel.require path
end
defs = plugin_definition_classes
extensions = defs.map { |desc| desc.new }
if block_given?
extensions.each do |ext|
yield ext
end
else
Enumerator.new do |yielder|
extensions.each do |ext|
yielder << ext
end
end
end
end
# Retrieves the objects that include {included_mod}, starting from a base
# {mod} module.
def plugin_definition_classes(
mod = Hardwire::Plugins,
included_mod = Hardwire::PluginDefinition::InstanceMethods)
targets = []
# Walk constants recursively.
mod.constants.each do |sym|
const = mod.const_get(sym)
if const.instance_of?(Class) &&
const.include?(included_mod) &&
contains_plugin?(const)
targets << const
elsif const.instance_of?(Module)
targets.concat(plugin_definition_classes(const, included_mod))
end
end
# Return objects that include? included_mod.
targets
end
# Finds the plugin definition paths on the {LOAD_PATH}.
def plugin_definition_paths
glob = Pathname(plugin_base_path) + "**" + "def.rb"
Gem.find_files_from_load_path(glob)
end
# Returns true if {plugin}'s path is defined within the container or false
# if it is not.
def contains_plugin?(plugin)
plugin_definition_paths.any? { |path|
path == File.expand_path(plugin.plugin_file_path)
}
end
end
end
module Hardwire
class Container
include Mixin::Query
attr_reader :plugin_base_path
def initialize(plugin_base_path)
@plugin_base_path = plugin_base_path
end
def use(id, options = {})
id_sym = id.to_s.to_sym
plugin = plugins.find { |p|
[p.identifiers].flatten.map(&:to_s).map(&:to_sym).include?(id_sym)
}
return nil unless plugin
begin
# Activate plugin.
plugin.activate
# Activation was successful because no exceptions were raised.
plugin.on_successful_activation
plugin.create(options)
rescue Exception => exception
# Catch all exceptions and pass to plugin to handle.
plugin.on_failed_activation(exception)
end
end
end
end
#
# Examples Only
#
module Hardwire::Plugins
module Foo
class FooPlugin
include Hardwire::PluginDefinition
def self.plugin_file_path
__FILE__
end
def identifiers
:foo
end
def name
:Foo
end
def create(options = {})
FooShazam.new
end
end
class FooShazam
def learn(skill)
puts "meditating..."
sleep 3
puts "* you know #{skill}"
end
end
end
end
module Hardwire::Plugins
module Bar
class BarPlugin
include Hardwire::PluginDefinition
def self.plugin_file_path
__FILE__
end
def identifiers
:bar
end
def name
:Bar
end
def create(options = {})
BarShazam.new
end
end
class BarShazam
def drink(number = 1)
number.times do
puts "gulp..."
sleep 3
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment