Class: Tap::Task

Inherits:
App::Api show all
Includes:
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]]

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

Instance Attribute Summary

Attributes included from App::Node

#joins

Attributes inherited from App::Api

#app

Class Method Summary collapse

Instance Method Summary collapse

Methods included from App::Node

extended, intern, #on_complete

Methods inherited from App::Api

build, help, inherited, parse, #to_spec

Methods included from Signals

#signal, #signal?

Methods included from Signals::ModuleMethods

included

Constructor Details

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

Initializes a new Task.



284
285
286
287
# File 'lib/tap/task.rb', line 284

def initialize(config={}, app=Tap::App.instance)
  @joins = []
  super
end

Class Method Details

.load_config(path) ⇒ Object

Recursively loads path into a nested configuration file.



176
177
178
179
180
181
182
183
# File 'lib/tap/task.rb', line 176

def load_config(path) # :nodoc:
  # 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

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

Same as parse, but removes arguments destructively.



162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/tap/task.rb', line 162

def parse!(argv=ARGV, app=Tap::App.instance)
  parser = self.parser
  
  # (note defaults are not added so they will not
  # conflict with string keys from a config file)
  argv = parser.parse!(argv, :add_defaults => false)
  enque = parser.config.delete('enque')
  instance = build({'config' => parser.nested_config}, app)
  
  instance.enq(*argv) if enque
  [instance, argv]
end

.parserObject



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/tap/task.rb', line 128

def parser
  opts = super
  
  # add option to print help
  opts.on!("--help", "Print this help") do
    lines = desc.kind_of?(Lazydoc::Comment) ? desc.wrap(77, 2, nil) : []
    lines.collect! {|line| "  #{line}"}
    unless lines.empty?
      line = '-' * 80
      lines.unshift(line)
      lines.push(line)
    end

    puts "#{self}#{desc.empty? ? '' : ' -- '}#{desc.to_s}"
    puts help
    puts "usage: tap run -- #{to_s.underscore} #{args}"
    puts          
    puts opts
    exit
  end
  
  opts.on('--enque', 'Manually enques self') do
    opts['enque'] = true
  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
  
  opts
end

Instance Method Details

#associationsObject



289
290
291
# File 'lib/tap/task.rb', line 289

def associations
  [nil, joins]
end

#call(*inputs) ⇒ Object



301
302
303
# File 'lib/tap/task.rb', line 301

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.



332
333
334
335
# File 'lib/tap/task.rb', line 332

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.



297
298
299
# File 'lib/tap/task.rb', line 297

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.



351
352
353
354
# File 'lib/tap/task.rb', line 351

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.



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

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

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

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



384
385
386
# File 'lib/tap/task.rb', line 384

def log(action, msg=nil, level=Logger::INFO)
  app.log(action, msg, level) { yield }
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.



359
360
361
362
# File 'lib/tap/task.rb', line 359

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.



326
327
328
# File 'lib/tap/task.rb', line 326

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.



339
340
341
342
343
344
345
346
347
# File 'lib/tap/task.rb', line 339

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.



378
379
380
381
# File 'lib/tap/task.rb', line 378

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.



368
369
370
371
# File 'lib/tap/task.rb', line 368

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