Class: Tap::Task

Inherits:
Object
  • Object
show all
Includes:
Configurable, App::Node
Defined in:
lib/tap/task.rb

Overview

Tasks are nodes that map to the command line. Tasks provide support for configuration, documentation, and provide helpers to build workflows.

Task Definition

Tasks specify executable code by overridding the process method in subclasses. The number of inputs to process corresponds to the inputs given to execute or enq.

class NoInput < Tap::Task
  def process(); []; end
end

class OneInput < Tap::Task
  def process(input); [input]; end
end

class MixedInputs < Tap::Task
  def process(a, b, *args); [a,b,args]; end
end

NoInput.new.execute                          # => []
OneInput.new.execute(:a)                     # => [:a]
MixedInputs.new.execute(:a, :b)              # => [:a, :b, []]
MixedInputs.new.execute(:a, :b, 1, 2, 3)     # => [:a, :b, [1,2,3]]

Tasks may be created with new, or with intern. Intern overrides process using a block that receives the task and the inputs.

no_inputs = Task.intern {|task| [] }
one_input = Task.intern {|task, input| [input] }
mixed_inputs = Task.intern {|task, a, b, *args| [a, b, args] }

no_inputs.execute                            # => []
one_input.execute(:a)                        # => [:a]
mixed_inputs.execute(:a, :b)                 # => [:a, :b, []]
mixed_inputs.execute(:a, :b, 1, 2, 3)        # => [:a, :b, [1,2,3]]

Configuration

Tasks are configurable. By default each task will be configured as specified in the class definition. Configurations may be accessed through config, or through accessors.

class ConfiguredTask < Tap::Task
  config :one, 'one'
  config :two, 'two'
end

t = ConfiguredTask.new
t.config                     # => {:one => 'one', :two => 'two'}
t.one                        # => 'one'
t.one = 'ONE'
t.config                     # => {:one => 'ONE', :two => 'two'}

Overrides and even unspecified configurations may be provided during initialization. Unspecified configurations do not have accessors.

t = ConfiguredTask.new(:one => 'ONE', :three => 'three')
t.config                     # => {:one => 'ONE', :two => 'two', :three => 'three'}
t.respond_to?(:three)        # => false

Configurations can be validated/transformed using an optional block.

Many common blocks are pre-packaged and may be accessed through the class method ‘c’:

class ValidatingTask < Tap::Task
  # string config validated to be a string
  config :string, 'str', &c.check(String)

  # integer config; string inputs are converted using YAML
  config :integer, 1, &c.yaml(Integer)
end 

t = ValidatingTask.new
t.string = 1                 # !> ValidationError
t.integer = 1.1              # !> ValidationError

t.integer = "1"
t.integer == 1               # => true

See the Configurable documentation for more information.

Subclassing

Tasks may be subclassed normally, but be sure to call super as necessary, in particular when overriding the following methods:

class Subclass < Tap::Task
  class << self
    def inherited(child)
      super
    end
  end

  def initialize(*args)
    super
  end

  def initialize_copy(orig)
    super
  end
end

Direct Known Subclasses

Tap::Tasks::Dump, Tap::Tasks::Load

Constant Summary collapse

DEFAULT_HELP_TEMPLATE =
%Q{<% desc = task_class::desc %>
<%= task_class %><%= desc.empty? ? '' : ' -- ' %><%= desc.to_s %>

<% desc = desc.kind_of?(Lazydoc::Comment) ? desc.wrap(77, 2, nil) : [] %>
<% unless desc.empty? %>
<%= '-' * 80 %>

<% desc.each do |line| %>
  <%= line %>
<% end %>
<%= '-' * 80 %>
<% end %>

}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes included from App::Node

#dependencies, #joins

Class Method Summary collapse

Instance Method Summary collapse

Methods included from App::Node

#depends_on, extended, #on_complete

Constructor Details

#initialize(config = {}, app = Tap::App.instance) ⇒ Task

Initializes a new Task.



407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/tap/task.rb', line 407

def initialize(config={}, app=Tap::App.instance)
  @app = app
  @joins = []
  @dependencies = []
  
  # initialize configs
  initialize_config(config)
  
  # setup class dependencies
  self.class.dependencies.each do |dependency_class|
    depends_on dependency_class.instance(app)
  end
end

Class Attribute Details

.dependenciesObject (readonly)

Returns class dependencies



128
129
130
# File 'lib/tap/task.rb', line 128

def dependencies
  @dependencies
end

Instance Attribute Details

#appObject (readonly)

The App receiving self during enq



404
405
406
# File 'lib/tap/task.rb', line 404

def app
  @app
end

Class Method Details

.desc(resolve = true) ⇒ Object



391
392
393
394
# File 'lib/tap/task.rb', line 391

def self.desc(resolve=true)
  comment = const_attrs['task'] ||= self.manifest
  resolve && comment.kind_of?(Lazydoc::Comment) ? comment.resolve : comment
end

.helpObject

Returns the class help.



236
237
238
# File 'lib/tap/task.rb', line 236

def help
  Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
end

.inherited(child) ⇒ Object

:nodoc:



135
136
137
138
139
140
141
142
143
# File 'lib/tap/task.rb', line 135

def inherited(child) # :nodoc:
  unless child.instance_variable_defined?(:@source_file)
    caller[0] =~ Lazydoc::CALLER_REGEXP
    child.instance_variable_set(:@source_file, File.expand_path($1)) 
  end
  
  child.instance_variable_set(:@dependencies, dependencies.dup)
  super
end

.instance(app = Tap::App.instance, auto_initialize = true) ⇒ Object

Returns or initializes the instance of self cached with app.



131
132
133
# File 'lib/tap/task.rb', line 131

def instance(app=Tap::App.instance, auto_initialize=true)
  app.cache[self] ||= (auto_initialize ? new({}, app) : nil)
end

.instantiate(argh = {}, app = Tap::App.instance) ⇒ Object

Instantiates an instance of self and returns an instance of self and an array of arguments (implicitly to be enqued to the instance).



205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/tap/task.rb', line 205

def instantiate(argh={}, app=Tap::App.instance)
  config = argh[:config] || {}
  instance = new(config, app)
  
  if argh[:cache]
    if app.cache.has_key?(self) && app.cache[self] != instance
      raise "cache already has an instance for: #{self}"
    end
  
    app.cache[self] = instance
  end
  
  instance
end

.intern(config = {}, app = Tap::App.instance, &block) ⇒ Object

Instantiates a new task with the input arguments and overrides process with the block. The block will be called with the task instance, plus any inputs.

Simply instantiates a new task if no block is given.



150
151
152
153
154
155
156
157
# File 'lib/tap/task.rb', line 150

def intern(config={}, app=Tap::App.instance, &block) # :yields: task, inputs...
  instance = new(config, app)
  if block_given?
    instance.extend Support::Intern(:process)
    instance.process_block = block
  end
  instance
end

.load_config(path) ⇒ Object

Recursively loads path into a nested configuration file.



241
242
243
244
245
246
247
248
# File 'lib/tap/task.rb', line 241

def load_config(path)
  # optimization to check for trivial paths
  return {} if Root::Utils.trivial?(path)
  
  Configurable::Utils.load_file(path, true) do |base, key, value|
    base[key] ||= value if base.kind_of?(Hash)
  end
end

.manifestObject



395
396
397
398
399
400
# File 'lib/tap/task.rb', line 395

def self.manifest
  # :::-
  #"warn manifest is depreciated, use ::task instead"
  # :::+
  const_attrs['manifest'] ||= Lazydoc::Subject.new(nil, lazydoc)
end

.parse(argv = ARGV, app = Tap::App.instance) ⇒ Object

Parses the argv into an instance of self. By default parse parses an argh then calls instantiate, but there is no requirement that this occurs in subclasses.



162
163
164
# File 'lib/tap/task.rb', line 162

def parse(argv=ARGV, app=Tap::App.instance)
  parse!(argv.dup, app)
end

.parse!(argv = ARGV, app = Tap::App.instance) ⇒ Object

Same as parse, but removes arguments destructively.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/tap/task.rb', line 167

def parse!(argv=ARGV, app=Tap::App.instance)
  opts = ConfigParser.new
  
  unless configurations.empty?
    opts.separator "configurations:"
    opts.add(configurations)
    opts.separator ""
  end
  
  opts.separator "options:"
  
  # add option to print help
  opts.on("--help", "Print this help") do
    prg = case $0
    when /rap$/ then 'rap'
    else 'tap run --'
    end
    
    puts "#{help}usage: #{prg} #{to_s.underscore} #{args}"
    puts          
    puts opts
    exit
  end
  
  # add option to specify a config file
  opts.on('--config FILE', 'Specifies a config file') do |config_file|
    opts.config.merge!(load_config(config_file))
  end
  
  # (note defaults are not added so they will not
  # conflict with string keys from a config file)
  argv = opts.parse!(argv, :add_defaults => false)
  
  instantiate({:config => opts.nested_config}, app)
end

Instance Method Details

#call(*inputs) ⇒ Object



429
430
431
# File 'lib/tap/task.rb', line 429

def call(*inputs)
  process(*inputs)
end

#enq(*inputs) ⇒ Object

Enqueues self to app with the inputs. The number of inputs provided should match the number of inputs for the method_name method.



460
461
462
463
# File 'lib/tap/task.rb', line 460

def enq(*inputs)
  app.queue.enq(self, inputs)
  self
end

#execute(*inputs) ⇒ Object

Auditing method call. Resolves dependencies, executes method_name, and sends the audited result to the on_complete_block (if set).

Returns the audited result.



425
426
427
# File 'lib/tap/task.rb', line 425

def execute(*inputs)
  app.dispatch(self, inputs)
end

#fork(*targets) ⇒ Object

Sets a fork workflow pattern for self; each target will enque the results of self.



479
480
481
482
# File 'lib/tap/task.rb', line 479

def fork(*targets)
  options = targets[-1].kind_of?(Hash) ? targets.pop : {}
  Join.new(options, app).join([self], targets)
end

#inspectObject

Provides an abbreviated version of the default inspect, with only the task class, object_id, and configurations listed.



518
519
520
# File 'lib/tap/task.rb', line 518

def inspect
  "#<#{self.class.to_s}:#{object_id} #{config.to_hash.inspect} >"
end

#log(action, msg = "", level = Logger::INFO) ⇒ Object

Logs the inputs to the application logger (via app.log)



512
513
514
# File 'lib/tap/task.rb', line 512

def log(action, msg="", level=Logger::INFO)
  app.log(action, msg, level)
end

#merge(*sources) ⇒ Object

Sets a simple merge workflow pattern for the source tasks. Each source enques self with it’s result; no synchronization occurs, nor are results grouped before being enqued.



487
488
489
490
# File 'lib/tap/task.rb', line 487

def merge(*sources)
  options = sources[-1].kind_of?(Hash) ? sources.pop : {}
  Join.new(options, app).join(sources, [self])
end

#process(*inputs) ⇒ Object

The method for processing inputs into outputs. Override this method in subclasses to provide class-specific process logic. The number of arguments specified by process corresponds to the number of arguments the task should have when enqued or executed.

class TaskWithTwoInputs < Tap::Task
  def process(a, b)
    [b,a]
  end
end

results = []
app = Tap::App.new {|result| results << result }

t = TaskWithTwoInputs.new({}, app)
t.enq(1,2).enq(3,4)

app.run
results                 # => [[2,1], [4,3]]

By default, process simply returns the inputs.



454
455
456
# File 'lib/tap/task.rb', line 454

def process(*inputs)
  inputs
end

#sequence(*tasks) ⇒ Object

Sets a sequence workflow pattern for the tasks; each task enques the next task with it’s results, starting with self.



467
468
469
470
471
472
473
474
475
# File 'lib/tap/task.rb', line 467

def sequence(*tasks)
  options = tasks[-1].kind_of?(Hash) ? tasks.pop : {}
  
  current_task = self
  tasks.each do |next_task|
    Join.new(options, app).join([current_task], [next_task])
    current_task = next_task
  end
end

#switch(*targets, &block) ⇒ Object

Sets a switch workflow pattern for self. On complete, switch yields the result to the block and the block should return the index of the target to enque with the results. No target will be enqued if the index is false or nil. An error is raised if no target can be found for the specified index. See Joins::Switch.



506
507
508
509
# File 'lib/tap/task.rb', line 506

def switch(*targets, &block) # :yields: result
  options = targets[-1].kind_of?(Hash) ? targets.pop : {}
  Joins::Switch.new(options, app).join([self], targets, &block)
end

#sync_merge(*sources) ⇒ Object

Sets a synchronized merge workflow for the source tasks. Results from each source are collected and enqued as a single group to self. The collective results are not enqued until all sources have completed. See Joins::Sync.



496
497
498
499
# File 'lib/tap/task.rb', line 496

def sync_merge(*sources)
  options = sources[-1].kind_of?(Hash) ? sources.pop : {}
  Joins::Sync.new(options, app).join(sources, [self])
end