Taskable
September 12, 2008
Taskable
On more than a few occasions I have taken a stab at writing a general purpose task system, akin to Rake’s, but one that works within the framework of Ruby class inheritance. I recall my first attempt was quite an unwieldy beast, and my subsequent attempts were fairly unwieldy too, but over time they became more concise. Below is the most concise variation yet, and I was wondering what other thought of it – do you see any flaws in the design; does it satisfy all the criteria of such a system; would you find it useful; etc.
One issue to note about the code, is the use of the *_trigger methods.
I’m not sure that’s the best approach. My original approach was
to provide a #run method (you can see it commented out), but this
requires dividing Taskable into two parts, a module for extending and
a module for including, and I try to avoid that design when I can.
module Taskable
def self.append_features(base)
base.extend(self)
end
# Without an argument, returns list of tasks defined for this class.
#
# If a task's target name is given, will return the first
# task matching the name found in the class' inheritance chain.
# This is important to ensure tasks are inherited in the same manner
# that methods are.
def tasks(target=nil)
if target
target = target.to_sym
anc = ancestors.select{|a| a < Taskable} #DSL
t = nil; anc.find{|a| t = a.tasks[target]}
return t
else
@tasks ||= {}
end
end
# Set a description to be used by then next defined task in this class.
def desc(description)
@desc = description
end
# Define a task.
def task(target_and_requisite, &function)
target, requisite, function = *Task.parse_arguments(target_and_requisite, &function)
task = tasks[target.to_sym] ||= (
tdesc = @desc
@desc = nil
Task.new(self, target, tdesc)
)
task.update(requisite, &function)
define_method("#{target}_trigger"){ task.run(self) } # or use #run?
define_method("#{target}:task", &function) # TODO: in 1.9 use instance_exec instead.
end
# Run a task.
# Hmmm... to add this would require another module (to include).
# But I'm not sure. Maybe trigger method is the better way?
#
#def run(target)
# #t = self.class.tasks(target)
# #t.run(self)
# send("#{target}_trigger")
#end
# = Task Class
#
class Task
attr :base
attr :target
attr :requisite
attr :function
attr :description
def initialize(base, target, description=nil, requisite=nil, &function)
@base = base
@target = target.to_sym
@description = description
@requisite = requisite || []
@function = function
end
#
def update(requisite, &function)
@requisite.concat(requisite).uniq!
@function = function if function
end
#
def prerequisite
base.ancestors.select{ |a| a.is_a?(Taskable) }.collect{ |a|
a.tasks[target].requisite
}.flatten.uniq
end
# invoke target
def run(object)
rd = rule_dag
rd.each do |t|
object.send("#{t}:task")
end
end
#
#def call(object)
# object.instance_eval(&function)
#end
# Collect task dependencies for running.
def rule_dag(cache=[])
prerequisite.each do |r|
next if cache.include?(r)
t = base.tasks[r]
t.rule_dag(cache)
#cache << dep
end
cache << target.to_s
cache
end
#
def self.parse_arguments(name_and_reqs, &action)
if Hash===name_and_reqs
target = name_and_reqs.keys.first.to_s
reqs = [name_and_reqs.values.first].flatten
else
target = name_and_reqs.to_s
reqs = []
end
return target, reqs, action
end
end
end
Here’s a very simple QEDoc demo/spec:
class Example
include Taskable
task :task_with_no_requisites do
cache << "task_with_no_requisites"
end
task :task_with_one_requisite => [:task_with_no_requisites] do
cache << "task_with_one_requisite"
end
def cache
@cache ||= []
end
end
example = Example.new
#example.run :task_with_no_requisites
example.task_with_no_requisites_trigger
example.cache.assert == ["task_with_no_requisites"]
example = Example.new
#example.run :task_with_one_requisite
example.task_with_one_requisite_trigger
example.cache.assert == ["task_with_no_requisites", "task_with_one_requisite"]
So, what do you think?
title : Taskable date : 2008-09-12 categories : [ruby] layout : post