Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Plugins: Yet another API rewrite

The old API required engines to require code from Concerto, which worked
will in ruby 1.8.7, but not 1.9.2. The new API uses a routes.rb-style
DSL block to communicate plugin information back to the main application
without such a dependency but is still able to survive class reloading.
The API has also been made more consistent, with _all_ configuration
taking place through the new PluginInfo object from within the plugin's
engine class.
  • Loading branch information...
commit a0eec3bb3b3ddefc833727918a93a09d99785beb 1 parent ec00592
@mikldt mikldt authored
View
170 app/models/concerto_plugin.rb
@@ -12,54 +12,10 @@
class ConcertoPlugin < ActiveRecord::Base
attr_accessible :enabled, :gem_name, :gem_version, :installed, :module_name, :name, :source, :source_url
- mattr_accessor :apps_to_mount
+ scope :enabled, where(:enabled => true)
- # Method to be called exactly once at app boot
- # Will iterate over all the enabled plugins, and attempt to
- # run their initialization code. This is a hook for plugins
- # to take care of their proper installation, set up configs,
- # set up hooks, and request routing if needed.
- def self.initialize_plugins
- method_name = "initialize_plugin"
- logger.info "ConcertoPlugin: Initializing Plugins"
- ConcertoPlugin.all.each do |plugin|
- if plugin.enabled?
- if Object.const_defined?(plugin.module_name)
- mod = plugin.module_name.constantize
- if mod.respond_to? method_name
- mod.method(method_name).call(plugin)
- else
- logger.warn(
- "ConcertoPlugin: Plugin #{plugin.name} does not respond to " +
- method_name + ", skipping initialization.")
- end
- else
- logger.warn(
- "ConcertoPlugin: Plugin #{plugin.name} module (" +
- plugin.module_name + ") not found. Skipping initialization.")
- end
- end
- end
- end
-
- # Used by the plugin initializer to register its own information
- # Options hash takes the same arguments as ConcertoConfig.make_concerto_config
- # except for the plugin_id field.
- def make_plugin_config(config_key, config_value, options={})
- options[:plugin_id] = id
- ConcertoConfig.make_concerto_config(config_key, config_value, options)
- end
-
- # Requests that the plugin be mounted as a rack app at
- # Rails.root/url_string
- # The plugin is responsible for setting its own engine name.
- # Should only be called by the plugin during initialization.
- def request_route(url_string, rack_app)
- self.apps_to_mount = self.apps_to_mount || []
- self.apps_to_mount << {:url_string => url_string, :rack_app => rack_app}
- end
-
- def self.get_engine(plugin_name)
+ private
+ def get_engine_from_module(plugin_name)
if Object.const_defined?(plugin_name)
mod = plugin_name.constantize
if mod.const_defined?("Engine")
@@ -71,58 +27,116 @@ def self.get_engine(plugin_name)
return false
end
else
- logger.warn("ConcertoPlugin: #{plugin+name} module (" +
+ logger.warn("ConcertoPlugin: #{plugin_name} module (" +
plugin_name + ") not found.")
return false
end
end
+ public
+
+ # Looks for the Engine in the module associated with this plugin.
+ def engine
+ @engine ||= get_engine_from_module(module_name)
+ end
+
+ # Returns the instance of PluginInfo provided by the engine
+ # Note for simplicity we're not caching this info.
+ def plugin_info
+ info = nil
+ if engine.respond_to? "plugin_info"
+ info = engine.plugin_info(Concerto::PluginInfo)
+ end
+ info
+ end
+
+ # Method to be called exactly once at app boot.
+ # Will iterate over all the enabled plugins, and run any
+ # initialization code they have specified.
+ def self.initialize_plugins
+ method_name = "initialize_plugin"
+ ConcertoPlugin.enabled.each do |plugin|
+ if info = plugin.plugin_info
+ if info.init_block.is_a? Proc
+ info.init_block.call
+ end
+ end
+ end
+ end
+
+ # Uses ConcertoConfig to initialize any configuration objects
+ # requested by enabled plugins. Called by initializer at boot.
+ def self.make_plugin_configs
+ configs = []
+ ConcertoPlugin.enabled.each do |plugin|
+ if info = plugin.plugin_info
+ (info.configs || []).each do |c|
+ c[:options][:plugin_id] = plugin.id
+ ConcertoConfig.make_concerto_config(
+ c[:config_key], c[:config_value], c[:options]
+ )
+ end
+ end
+ end
+ end
+
+ # Finds all the requested engine mount points and associated
+ # info, and returns the corressponding array of hashes.
+ def self.get_mount_points
+ mount_points = []
+ ConcertoPlugin.enabled.each do |plugin|
+ if info = plugin.plugin_info
+ if info.mount_points.is_a? Array
+ mount_points += info.mount_points
+ end
+ end
+ end
+ mount_points
+ end
+
+ # Given the view's rendering context, renders all the requests for
+ # the given hook from enabled plugins, and returns the resulting
+ # string.
# This is very inefficient, especially for multiple hooks in one view.
# However, the API is implementation-agnostic.
def self.render_view_hook(context, hook_name)
result = ""
controller_name = context.controller.controller_name
- ConcertoPlugin.all.each do |plugin|
- if plugin.enabled?
- if engine = get_engine(plugin.module_name)
- engine.get_view_hooks(controller_name, hook_name).each do |hook|
- if hook[:type] == :partial
- result += context.render :partial => hook[:hook]
- elsif hook[:type] == :text
- result += hook[:hook]
- elsif hook[:type] == :proc
- result += context.instance_eval(&hook[:hook])
- end
- result += "\n"
+ ConcertoPlugin.enabled.each do |plugin|
+ if info = plugin.plugin_info
+ info.get_view_hooks(controller_name, hook_name).each do |hook|
+ if hook[:type] == :partial
+ result += context.render :partial => hook[:hook]
+ elsif hook[:type] == :text
+ result += hook[:hook]
+ elsif hook[:type] == :proc
+ result += context.instance_eval(&hook[:hook])
end
- else
- logger.warn("ConcertoPlugin: Failed to check view hooks for "+
- "#{plugin.name}")
+ result += "\n"
end
+ else
+ logger.warn("ConcertoPlugin: Failed to check view hooks for "+
+ "#{plugin.name}")
end
end
return result.html_safe
end
+ # Installs all the code requested by enabled plugins for hooks
+ # in the given controller as callbacks which can then be triggered
+ # by code in the controller.
def self.install_callbacks(controller)
method_name = "get_controller_hooks"
callbacks = []
- ConcertoPlugin.all.each do |plugin|
- if plugin.enabled?
- if engine = get_engine(plugin.module_name)
- if engine.respond_to? method_name
- controller_callbacks = engine.instance.method(method_name).call(controller.name)
- if controller_callbacks.is_a? Array
- callbacks += controller_callbacks
- end
- else
- logger.warn("ConcertoPlugin: #{plugin.name} does not respond to "+
- method_name + ", skipping callbacks.")
- end
- else
- logger.warn("ConcertoPlugin: failed to check #{plugin.name}" +
- " for callbacks")
+ ConcertoPlugin.enabled.each do |plugin|
+ if info = plugin.plugin_info
+ controller_callbacks = info.get_controller_hooks(controller.name)
+ if controller_callbacks.is_a? Array
+ callbacks += controller_callbacks
end
+ else
+ logger.warn("ConcertoPlugin: failed to check #{plugin.name}" +
+ " for callbacks")
end
end
logger.info "ConcertoPlugin: Callbacks for this controller listed below:"
View
18 config/initializers/12-plugins.rb
@@ -1,21 +1,25 @@
# Concerto Plugins Initializer
# This initializer assumes that all gems have been loaded
-# and that the database is fully migrated and ActivRecord
+# and that the database is fully migrated and ActiveRecord
# is connected to it.
-# First enumerate plugins that are installed and enabled,
-# and then run their initialization code. This will include
-# installation tasks, setting up configs, and requesting
-# routes.
+# First register any plugin configs.
+# This will cause ConcertoPlugin to call the ConcertoConfig
+# class for all of the enabled plugins.
+ConcertoPlugin.make_plugin_configs
+
+# Next run any boot-time initialization code that the
+# plugins might need.
ConcertoPlugin.initialize_plugins
# Mount all the engines at their requested mount points.
# In the future, this may perform more strict checking to
# make sure that the plugin is enabled and the app exists.
Rails.application.routes.append do
- if ConcertoPlugin.apps_to_mount.is_a? Array
- ConcertoPlugin.apps_to_mount.each do |app|
+ mount_points = ConcertoPlugin.get_mount_points
+ if mount_points.is_a? Array
+ mount_points.each do |app|
mount app[:rack_app] => "/" + app[:url_string], :as => app[:url_string]
# e.g. mount ConcertoHardware::Engine => "/hardware"
end
View
121 lib/concerto/plugin_info.rb
@@ -0,0 +1,121 @@
+# The PluginInfo class is instantiated in each Concerto Plugin
+# so that it can share information about itself with the main
+# Concerto Application.
+#
+# The methods provide below provide a two-way API: a set of
+# methods to be called within the Engine class to configure
+# callbacks and the like; and a set of methods to be called
+# by the ConcertoPlugin model when interfacing to the rest of
+# the Concerto application.
+
+module Concerto
+ class PluginInfo
+ # These instance variables will be set by the configuration
+ # API, but readable from the main application.
+ attr_reader :controller_hooks
+ attr_reader :view_hooks
+ attr_reader :mount_points
+ attr_reader :configs
+ attr_reader :init_block
+
+ # Configuration API: Accessible by the engine via "new"
+
+ # Provide a convenient way for the engine to configure plugin
+ # settings using the methods provided below.
+ def initialize (&block)
+ instance_exec(&block)
+ end
+
+ private # the following methods will be accessed via instance_exec
+
+ # Requests that the plugin be mounted as a rack app at
+ # Rails.root/url_string
+ # Usually there will only be one per plugin, but hey, we're flexible.
+ # The plugin is responsible for setting its own engine name.
+ def add_route(url_string, rack_app)
+ @mount_points = @mount_points || []
+ @mount_points << {:url_string => url_string, :rack_app => rack_app}
+ end
+
+ # Add a configuration item to the database for this plugin
+ def add_config(config_key, config_value, options)
+ @configs = @configs || []
+ @configs << {:config_key => config_key,
+ :config_value=> config_value,
+ :options => options}
+ end
+
+ # Set some code to run when the app first boots up,
+ # if the plugin is enabled.
+ def init (&block)
+ @init_block = block
+ end
+
+
+ # Add a hook into a callback in the given controller.
+ def add_controller_hook(controller_name, name, filter_list, &block)
+ @controller_hooks ||= []
+ @controller_hooks << {
+ :controller_name => controller_name,
+ :name => name,
+ :filter_list => filter_list,
+ :block => block
+ }
+ end
+
+ # Add code into the main application's UI where a hook has been
+ # defined. The hook nay take a number of forms, including static
+ # text, a partial, or a code block for later execution.
+ def add_view_hook(controller_name, hook_sym, options={})
+ @view_hooks ||= []
+ if options.has_key? :partial
+ mytype=:partial
+ myhook=options[:partial]
+ elsif options.has_key? :text
+ mytype=:text
+ myhook=options[:text]
+ elsif options.has_key? :proc
+ mytype=:proc
+ myhook=options[:proc]
+ elsif block_given?
+ mytype = :proc
+ myhook = Proc.new # takes the value of the passed block
+ elsif not hook
+ raise "error: add_view_hook missing a block, partial, or text!"
+ end
+
+ @view_hooks << {
+ :controller_name => controller_name,
+ :sym => hook_sym,
+ :type => mytype,
+ :hook => myhook
+ }
+ end
+
+ # Info Reading API
+ # Accessed by ConcertoPlugin in the main application
+ public
+
+ # Returns an array of hashes specifying all of the requested
+ # hooks into the given controller.
+ def get_controller_hooks(controller_name)
+ return @controller_hooks.select do |h|
+ h[:controller_name] == controller_name
+ end
+ end
+
+ # Returns an array of hashes specifying all of the requested
+ # includes for the given hook.
+ def get_view_hooks(controller_name, hook_sym)
+ hooks = []
+ @view_hooks.each do |hook|
+ if hook[:controller_name] = controller_name
+ if hook[:sym] == hook_sym
+ hooks << hook
+ end
+ end
+ end
+ return hooks
+ end # get_view_hooks
+ end # PluginInfo
+end # Concerto
View
75 lib/concerto/plugin_library.rb
@@ -1,75 +0,0 @@
-# The PluginLibrary class is not used directly in Concerto, but
-# is provided to plugins to include into their Engines for
-#
-# The methods provide below provide a two-way API: a set of
-# methods to be called within the Engine class to configure
-# callbacks and the like; and a set of methods to be called
-# by the ConcertoPlugin model when interfacing to the rest of
-# the Concerto application.
-
-# TODO: pull in plugin initialization stuff (config items, route)
-
-module Concerto
- module PluginLibrary
- module ClassMethods
- attr_accessor :controller_hooks
- attr_accessor :view_hooks
-
- # TODO: test with multiple callbacks
- def add_controller_hook(controller_name, name, filter_list, &block)
- @controller_hooks ||= []
- @controller_hooks << {
- :controller_name => controller_name,
- :name => name,
- :filter_list => filter_list,
- :block => block
- }
- end
-
- def get_controller_hooks(controller_name)
- return @controller_hooks.select do |h|
- h[:controller_name] == controller_name
- end
- end
-
- def add_view_hook(controller_name, hook_sym, options={})
- @view_hooks ||= []
- if options.has_key? :partial
- mytype=:partial
- myhook=options[:partial]
- elsif options.has_key? :text
- mytype=:text
- myhook=options[:text]
- elsif options.has_key? :proc
- mytype=:proc
- myhook=options[:proc]
- elsif block_given?
- mytype = :proc
- myhook = Proc.new # takes the value of the passed block
- elsif not hook
- raise "error: add_view_hook missing a block, partial, or text!"
- end
-
- @view_hooks << {
- :controller_name => controller_name,
- :sym => hook_sym,
- :type => mytype,
- :hook => myhook
- }
- end
-
- def get_view_hooks(controller_name, hook_sym)
- hooks = []
- @view_hooks.each do |hook|
- if hook[:controller_name] = controller_name
- if hook[:sym] == hook_sym
- hooks << hook
- end
- end
- end
- return hooks
- end # get_view_hooks
-
- end # ClassMethods
- end # PluginLibrary
-end # Concerto
Please sign in to comment.
Something went wrong with that request. Please try again.