TRANSCODE Explorations into the Code Transcendental.

Taskable

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