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(/<(.*?)>/, '&lt;\1&gt;')
  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