Tap (Task Application)

A framework for creating configurable, distributable tasks and workflows. Tap allows tasks to act like command line executables, but with the ability to pass any object as an input/output (not just stdin/stdout). Moreover any join logic may be used in workflows (not just pipe) so forks, merges, and synchronization are all possible. Tap supports signaling objects in the workflow, allowing interrogation and manipulation of long-running workflows.

All that said, Tap is often used as a really advanced Rake for providing toolkits to developers.

A quick look

Tasks are defined with simple declarations:

  # Constructs a configurable message for the input.
  desc "make a goodnight message"
  task :goodnight, :msg => 'goodnight' do |config, input|
    "#{config.msg} #{input}"

Tasks may be linked into workflows and configured individually:

  % tap goodnight moon -: dump
  goodnight moon
  % tap goodnight world --msg hello -: dump
  hello world

Workflows support the use of middleware to wrap the execution of each task, most commonly for logging and/or debugging.

  % tap goodnight moon -: dump --/use debugger
    21:06:53       0 << ["moon"] (Goodnight)
    21:06:53       0 >> "goodnight moon" (Goodnight)
    21:06:53       1 << "goodnight moon" (Tap::Tasks::Dump)
  goodnight moon
    21:06:53       1 >> "goodnight moon" (Tap::Tasks::Dump)

Tap also generates copious help.

  % tap goodnight --help
  Tapfile::Goodnight -- make a goodnight message
    Constructs a configurable message for the input.
  usage: tap tapfile/goodnight arg

          --msg MSG

          --help                       Print this help
          --config FILE                Specifies a config file

As workflows grow more complex, they can be declared and re-used much like tasks. Both task and workflow declarations define classes that can be instantiated and tested like any other plain-old-ruby object. In fact, oftentimes tasks are defined directly as classes, without the declaration sugar.

  require 'tap/task'
  # ::task your basic goodnight moon task
  # Says goodnight with a configurable message.
  class Goodnight < Tap::Task
    config :msg, 'goodnight'           # a goodnight message
    def process(name)
      "#{msg} #{name}"
  require 'tap/test/unit'
  require 'goodnight'
  class GoodnightTest < Test::Unit::TestCase
    def test_goodnight_makes_the_configurable_message
      task = Goodnight.new
      assert_equal 'goodnight moon', task.process('moon')
      task = Goodnight.new :msg => 'hello'
      assert_equal 'hello world', task.process('world')

In this form tasks can be distributed in a gem as normal. Tap discovers tasks in all gems that contain a ‘tap.yml’ file, although this behavior is customizable to limit the gems or individual tasks available in a workflow. Now lets have some fun.

  desc "Iteration of inputs (copy-paste command line syntax)"
  work :iterate, %{
    -   load/yaml
    -:i goodnight
    -:  dump
  desc "Forking and merging (literal definition of joins)"
  work :fork_and_merge, %{
    - load
    - goodnight --msg hi
    - goodnight --msg bye
    - dump
    - join 0 1,2
    - join 1,2 3
  desc "Out of order, synchronized Merge (showcases full syntax)"
  work :ooo_sync, %{
    - dump
    - goodnight
    - goodnight
    - load
    - join 3 1,2
    - sync 1,2 0
    :one => 'hi',
    :two => 'bye'
  } do |config|
    node(1).msg = config.one
    node(2).msg = config.two
    node(3)  # the return is the entry point for the workflow, ie 'load'

And here we go…

  % tap iterate "[moon, mittens, 'little toy boat']"
  goodnight moon
  goodnight mittens
  goodnight little toy boat
  % tap fork_and_merge moon
  hi moon
  bye moon
  % tap ooo_sync moon
  ["hi moon", "bye moon"]
  % tap ooo_sync moon --one hello --two goodnight
  ["hello moon", "goodnight moon"]

See the Tutorial and Documentation for more information.