Adding plugins to your Ruby application
November 23rd, 2006
is quite intelligent fellow. He seems to be a true Master not only in Ruby Language, but also in theoretical computing and mathematics. Every time I really read his blog I learn something new, not only from Ruby but about programming algorithms in general. I just wish I get to be in RubyFoo conf someday and have a chance to discuss with him in real life..
Recently I discovered his article about plugins in Ruby and I was astonished by his cleverness (after understanding the code, which took me a while even though it was short, or maybe because of it).
However, to be practical in large applications I think plugins should contain information about the extension points they apply to (at least for clarity, if not for other reasons). Another problem with plugins is that sometimes they have to be executed in some specific order. For example, a text formatting plugin may produce (X)HTML, and another formatting plugin might replace ‘<>’ characters with respective HTML entities.
For this reason, I did few simple additions to lend for correct order of execution. Finding out the correct order is very easy thanks to built-in Ruby class TSort, which stands for topological sort. You know, sorting stuff so that all dependencies come before stuff depending on them. Math buffs would talk about linear ordering in directed acyclic graphs. But I digress..
First, a hash is a handy replacement for more fancy-schmancy graph data structure containing topological information. So we start with
1 2 3 4 5 6 7 8 9 10 |
require 'tsort' class Hash include TSort alias tsort_each_node each_key # hash values contain dependencies def tsort_each_child(node, &block) fetch(node, []).each(&block) end end |
now class Hash responds to method tsort appropriately. Next I wanted to have a method `Plugin#extensions(ext_point)` which returns all registered plugins extending ext_point:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def self.extensions(ext_point) plugins = Plugin.registered_plugins.select do |n,o| o.extends == ext_point end deps = Hash.new([]) plugins.each do |name,obj| if obj.run_after deps[name] = obj.run_after.to_s elsif obj.run_before if obj.run_before.respond_to?(:pop) obj.run_before.each {|d| deps[d.to_s] = name} else deps[obj.run_before.to_s] = name end end end deps.tsort.map {|name| Plugin.registered_plugins[name]} end extend PluginSugar def_field :author, :version, :extends, :run_before, :run_after |
First we selected only interesting plugins. Then we create dependency hash in `deps` handling plugin attributes `:run_before` and `:run_after` and use topologically sorted hash for returning plugins to extension point in correct order.
Also note new fields added using `def_field`. How does one use it, then? very easy. Assume the following plugin in your application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Plugin.define "escape_plugin" do author "EdvardM" version "1.0.0" extends :output run_before :font_styler_plugin def format(str) str.gsub(/<(.*?)>/, '<\1>') end end ## somewhere in the application itself: Plugin.extensions(:output).each do |plugin| blog_content = plugin.format(blog_content) end |
Note that the whole “plugin framework” is still less than 66 lines of code. Complete code is attached below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
require 'tsort' class Hash include TSort alias tsort_each_node each_key # hash values contain dependencies def tsort_each_child(node, &block) fetch(node, []).each(&block) end end module PluginSugar # allow accessing attributes using def_field def def_field(*names) class_eval do names.each do |name| define_method(name) do |*args| case args.size when 0: instance_variable_get("@#{name}") else instance_variable_set("@#{name}", *args) end end end end end end class Plugin @registered_plugins = {} class << self attr_reader :registered_plugins private :new end def self.extensions(ext_point) plugins = Plugin.registered_plugins.select do |n,o| o.extends == ext_point end deps = Hash.new([]) plugins.each do |name,obj| if obj.run_after deps[name] = obj.run_after.to_s elsif obj.run_before if obj.run_before.respond_to?(:pop) obj.run_before.each {|d| deps[d.to_s] = name} else deps[obj.run_before.to_s] = name end end end deps.tsort.map {|name| Plugin.registered_plugins[name]} end def self.define(name, &block) p = new p.instance_eval(&block) Plugin.registered_plugins[name] = p end extend PluginSugar def_field :author, :version, :extends, :run_before, :run_after end |
Also note that you can add multiple plugins after `:run_before` or `:run_after`, eg.
`:run_before [:html_formatter, :escape_plugin]`

Leave a Reply