# TITLE: # # Taskable # # COPYRIGHT: # # Copyright (c) 2006 Thomas Sawyer # # LICENSE: # # Ruby License # # This module is free software. You may use, modify, and/or redistribute this # software under the same terms as Ruby. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # # AUTHORS: # # - Thomas Sawyer # # NOTES: # # - TODO The included call back does a comparison to Object. # This is a bit of a hack b/c there is actually no way to # check if it is the toplevel --a flaw w/ Ruby's toplevel proxy. # # - TODO Should Rake's namespace feature be added? This could interfer # with other definitions of #namespace. # # - TODO The only reason the :exec method is defined is b/c instance_exec # is not in Ruby yet, so until then this is the working hack. # # LOG: # # - CHANGE 2006-11-14 trans # # Taskable has been completely rewritten. While it is essentially # compatible with the previous implementation, it is not 100% the # same; mainly in that tasks are not defined as methods any longer. # This new implementation is now nearly 100% compatible with Rake's # design. Note, for a basic "taskable" system, more like the old # version, see depend.rb. require 'facets/class_extension' $toplevel = self # = Taskable # # The Taskable module provides a generic task system # patterned after Rake, but useable in any # code context --not just with the Rake tool. In other # words one can create methods with dependencies. # # NOTE Unlike methods, tasks can't take independent parameters # if they are to be used as prerequisites. The arguments passed # to a task call will also be passed to it's prequisites. # # To use Taskable at the toplevel use: # # include Taskable # # Or if you want all modules to be "taskable": # # class Module # include Taskable # end module Taskable def self.included( base ) if base == Object #$toplevel require 'facets/more/main_as_module.rb' Module.module_eval{ include TaskableDSL } else base.extend TaskableDSL end end ### CLASS LEVEL ### module TaskableDSL #-- # TODO Add task namespace functionality ??? #++ #def namespace #end # Define description for subsequent task. def desc(line=nil) return @_last_description unless line @_last_description = line.gsub("\n",'') end # Use up the description for subsequent task. def desc! l, @_last_description = @_last_description, nil l end # Task # # def task( target_to_source, &build ) target, source = *Task.parse(target_to_source) define_method("#{target}:exec",&build) if build (@task||={})[target] = Task.new(target, source, desc!, &build) end # File task # # Task must be provide instructions for building the file. def file( file_to_source, &build ) file, source = *Task.parse(file_to_source) define_method("#{file}:exec",&build) if build (@task||={})[file] = FileTask.new(file, source, desc!, &build) end # Rule task # # Task must be provide instructions for building the file(s). def rule( pattern_to_source, &build ) pattern, source = *Task.parse(pattern_to_source) define_method("#{pattern}:exec",&build) if build (@task||={})[pattern] = RuleTask.new(pattern, source, desc!, &build) end # def instance_tasks( ancestry=true ) @task ||= {} if ancestry ancestors.inject(@task.keys) do |m,a| t = a.instance_variable_get("@task") m |= t.keys if t m end else @task.keys end end # List of task names with descriptions. def described_tasks( ancestry=true ) memo = [] instance_tasks(ancestry).each do |name| memo << name if @task[name].desc end return memo end # List of task names without descriptions. def undescribed_tasks( ancestry=true ) memo = [] instance_tasks(ancestry).each do |name| memo << name unless @task[name].desc end return memo end # Find matching task. #-- # TODO Maybe this isn't really needed here and can be moved to Task class ??? #++ def instance_task( match ) hit = (@task||={}).values.find do |task| task.match(match) end return hit if hit ancestors.each do |a| task_table = a.instance_variable_get("@task") next unless task_table hit = task_table.values.find do |task| task.match(match) end break hit if hit end hit end end ### INSTANCE LEVEL ### # def tasks (class << self; self; end).instance_tasks end # def task(name) (class << self; self; end).instance_task(name) end # FIXME, THIS STILL WONT WORK AT TOPLEVEL!!!! def method_missing(t, *a, &b) p t p tasks p task(t) #if self.class.respond_to?(:instance_task) && (task = self.class.instance_task(t)) if tsk = task(t) tsk.run(self, t) else super(t.to_sym,*a,&b) end end end # class Taskable::Task # Parse target => [source,...] argument. def self.parse(target_to_source) if Hash === target_to_source target = target_to_source.keys[0] source = target_to_source.values[0] else target = target_to_source source = [] end return target.to_sym, source.collect{|s| s.to_sym} end # attr_reader :target, :source, :desc, :build alias :name :target alias :description :desc # New task. def initialize( target, source, desc=nil, &build ) @target = target @source = source @desc = desc @build = build end # Run task in given context. def run( context, target ) task = self source = @source build = @build presource(context).each do |d| d.call(context) end call(context) end # Call build exec of task. Note that the use of :exec method # is due to the lack of #instance_exec which will come wiht Ruby 1.9. def call( context ) context.send("#{@target}:exec", self) if @build end # def match( target ) @target.to_s == target.to_s end # Compile list of all unique prerequisite sources. def presource( context, build=[] ) @source.each do |s| t = context.class.instance_task(s) raise NoMethodError, 'undefined source' unless t build.unshift(t) t.presource(context,build) end build.uniq! build end end # class Taskable::FileTask < Taskable::Task # Run file task in a given context. def run( context, target ) task = self source = @source build = @build context.instance_eval do needed = false if File.exist?(file) #source.each { |s| send(s) if respond_to?(s) } timestamp = File.mtime(file) needed = source.any? { |f| File.mtime(f.to_s) > timestamp } else timestamp = Time.now - 1 needed = true end if needed build.call(task) unless File.exist?(file) and File.mtime(file) > timestamp raise "failed to build -- #{file}" end end end end # def match(target) @target.to_s == target.to_s end end # class Taskable::RuleTask < Taskable::FileTask # Run rule task in given context. def run( context, target ) @target = target super(context) end # def match(target) case @target when Regexp @target =~ target.to_s when String #if @target.index('*') #TODO # /#{@target.gsub('*', '.*?')}/ =~ target if @target.index('.') == 0 /#{Regexp.escape(@target)}$/ =~ target else super end else super end end end # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin ##test require 'test/unit' class TestTaskable1 < Test::Unit::TestCase module M include Taskable task :m1 => [ :c1 ] do @x << "m1" end task :m2 => [ :c2 ] do @x << "m2" end task :m3 => [ :c3 ] do @x << "m3" end task :m4 => [ :c1, :c2, :c3 ] do @x << "m4" end task :m5 do @x << 'm5' end end class B include Taskable attr :x def initialize ; @x = [] ; end desc "test-b1" task :b1 do @x << "b1" end task :b2 => [ :b1 ] end class C include M attr :x def initialize ; @x = [] ; end task :c1 do @x << "c1" end task :c2 => [ :c1, :c1 ] do @x << "c2" end task :c3 => [ :c1, :c2 ] do @x << "c3" end task :c4 => [ :m1 ] do @x << "c4" end task :c5 => [ :c5 ] do @x << "c5" end task :c6 => [ :c7 ] do @x << "c6" end task :c7 => [ :c6 ] do @x << "c7" end end class D < C task :d1 => [ :c1 ] do @x << "d1" ; end task :d2 => [ :m1 ] do @x << "d2" ; end end module N include M end class E include N attr :x def initialize ; @x = [] ; end task :e1 => [ :c1 ] do @x << "e1" ; end task :e2 => [ :m1 ] do @x << "e2" ; end task :e3 => [ :m5 ] do @x << "e3" ; end end module O include Taskable attr :x task :o1 do (@x||=[]) << "o1" end task :o2 => [ :o1 ] do (@x||=[]) << "o2" end end # tests def test_001 assert( B.described_tasks.include?(:b1) ) end def test_B1 b = B.new ; b.b1 assert_equal( [ 'b1' ], b.x ) end def test_B2 b = B.new ; b.b2 assert_equal( [ 'b1' ], b.x ) end def test_C1 c = C.new ; c.c1 assert_equal( [ 'c1' ], c.x ) end def test_C2 c = C.new ; c.c2 assert_equal( [ 'c1', 'c2' ], c.x ) end def test_C3 c = C.new ; c.c3 assert_equal( [ 'c1', 'c2', 'c3' ], c.x ) end def test_C4 c = C.new ; c.c4 assert_equal( [ 'c1', 'm1', 'c4' ], c.x ) end def test_M1 c = C.new ; c.m1 assert_equal( [ 'c1', 'm1' ], c.x ) end def test_M2 c = C.new ; c.m2 assert_equal( [ 'c1', 'c2', 'm2' ], c.x ) end def test_M3 c = C.new ; c.m3 assert_equal( [ 'c1', 'c2', 'c3', 'm3' ], c.x ) end def test_M4 c = C.new ; c.m4 assert_equal( [ 'c1', 'c2', 'c3', 'm4' ], c.x ) end def test_D1 d = D.new ; d.d1 assert_equal( [ 'c1', 'd1' ], d.x ) end def test_D2 d = D.new ; d.d2 assert_equal( [ 'c1', 'm1', 'd2' ], d.x ) end def test_E1 e = E.new assert_raises( NoMethodError ) { e.e1 } #assert_equal( [ 'c1', 'e1' ], e.x ) end def test_E2 e = E.new assert_raises( NoMethodError ) { e.e2 } #assert_equal( [ 'c1', 'm1', 'e2' ], e.x ) end def test_E3 e = E.new ; e.e3 assert_equal( [ 'm5', 'e3' ], e.x ) end # def test_F1 # F.o1 # assert_equal( [ 'o1' ], F.x ) # end # # def test_F2 # F.o2 # assert_equal( [ 'o1', 'o1', 'o2' ], F.x ) # end end =end ## # Test toplevel usage. # include Taskable p Object.ancestors task :foo do "foo" end task :bar => [ :foo ] do "bar" end #class TestTaskable2 #< Test::Unit::TestCase def test_01 puts foo end def test_02 puts bar end #end test_01 test_02 #=end # Author:: Thomas Sawyer # Copyright:: Copyright (c) 2006 Thomas Sawyer # License:: Ruby License