tap 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +71 -0
  3. data/Rakefile +117 -0
  4. data/bin/tap +63 -0
  5. data/lib/tap.rb +15 -0
  6. data/lib/tap/app.rb +739 -0
  7. data/lib/tap/file_task.rb +354 -0
  8. data/lib/tap/generator.rb +29 -0
  9. data/lib/tap/generator/generators/config/USAGE +0 -0
  10. data/lib/tap/generator/generators/config/config_generator.rb +23 -0
  11. data/lib/tap/generator/generators/config/templates/config.erb +2 -0
  12. data/lib/tap/generator/generators/file_task/USAGE +0 -0
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
  16. data/lib/tap/generator/generators/root/USAGE +0 -0
  17. data/lib/tap/generator/generators/root/root_generator.rb +36 -0
  18. data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
  19. data/lib/tap/generator/generators/root/templates/app.yml +19 -0
  20. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
  21. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
  22. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  23. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
  24. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
  25. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
  26. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
  27. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
  28. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
  29. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
  30. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  31. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
  32. data/lib/tap/generator/generators/task/USAGE +0 -0
  33. data/lib/tap/generator/generators/task/task_generator.rb +21 -0
  34. data/lib/tap/generator/generators/task/templates/task.erb +21 -0
  35. data/lib/tap/generator/generators/task/templates/test.erb +29 -0
  36. data/lib/tap/generator/generators/workflow/USAGE +0 -0
  37. data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
  38. data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
  39. data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
  40. data/lib/tap/generator/options.rb +26 -0
  41. data/lib/tap/generator/usage.rb +26 -0
  42. data/lib/tap/root.rb +275 -0
  43. data/lib/tap/script/console.rb +7 -0
  44. data/lib/tap/script/destroy.rb +8 -0
  45. data/lib/tap/script/generate.rb +8 -0
  46. data/lib/tap/script/run.rb +111 -0
  47. data/lib/tap/script/server.rb +12 -0
  48. data/lib/tap/support/audit.rb +415 -0
  49. data/lib/tap/support/batch_queue.rb +165 -0
  50. data/lib/tap/support/combinator.rb +114 -0
  51. data/lib/tap/support/logger.rb +91 -0
  52. data/lib/tap/support/rap.rb +38 -0
  53. data/lib/tap/support/run_error.rb +20 -0
  54. data/lib/tap/support/template.rb +81 -0
  55. data/lib/tap/support/templater.rb +155 -0
  56. data/lib/tap/support/versions.rb +63 -0
  57. data/lib/tap/task.rb +448 -0
  58. data/lib/tap/test.rb +320 -0
  59. data/lib/tap/test/env_vars.rb +16 -0
  60. data/lib/tap/test/inference_methods.rb +298 -0
  61. data/lib/tap/test/subset_methods.rb +260 -0
  62. data/lib/tap/version.rb +3 -0
  63. data/lib/tap/workflow.rb +73 -0
  64. data/test/app/config/addition_template.yml +6 -0
  65. data/test/app/config/batch.yml +2 -0
  66. data/test/app/config/empty.yml +0 -0
  67. data/test/app/config/erb.yml +1 -0
  68. data/test/app/config/template.yml +6 -0
  69. data/test/app/config/version-0.1.yml +1 -0
  70. data/test/app/config/version.yml +1 -0
  71. data/test/app/lib/app_test_task.rb +2 -0
  72. data/test/app_class_test.rb +33 -0
  73. data/test/app_test.rb +1372 -0
  74. data/test/file_task/config/batch.yml +2 -0
  75. data/test/file_task/config/configured.yml +1 -0
  76. data/test/file_task/old_file_one.txt +0 -0
  77. data/test/file_task/old_file_two.txt +0 -0
  78. data/test/file_task_test.rb +1041 -0
  79. data/test/root/alt_lib/alt_module.rb +4 -0
  80. data/test/root/lib/absolute_alt_filepath.rb +2 -0
  81. data/test/root/lib/alternative_filepath.rb +2 -0
  82. data/test/root/lib/another_module.rb +2 -0
  83. data/test/root/lib/nested/some_module.rb +4 -0
  84. data/test/root/lib/no_module_included.rb +0 -0
  85. data/test/root/lib/some/module.rb +4 -0
  86. data/test/root/lib/some_class.rb +2 -0
  87. data/test/root/lib/some_module.rb +3 -0
  88. data/test/root/load_path/load_path_module.rb +2 -0
  89. data/test/root/load_path/skip_module.rb +2 -0
  90. data/test/root/mtime/older.txt +0 -0
  91. data/test/root/unload/full_path.rb +2 -0
  92. data/test/root/unload/loaded_by_nested.rb +2 -0
  93. data/test/root/unload/nested/nested_load.rb +6 -0
  94. data/test/root/unload/nested/nested_with_ext.rb +4 -0
  95. data/test/root/unload/nested/relative_path.rb +4 -0
  96. data/test/root/unload/older.rb +2 -0
  97. data/test/root/unload/unload_base.rb +9 -0
  98. data/test/root/versions/another.yml +0 -0
  99. data/test/root/versions/file-0.1.2.yml +0 -0
  100. data/test/root/versions/file-0.1.yml +0 -0
  101. data/test/root/versions/file.yml +0 -0
  102. data/test/root_test.rb +483 -0
  103. data/test/support/audit_test.rb +449 -0
  104. data/test/support/batch_queue_test.rb +320 -0
  105. data/test/support/combinator_test.rb +249 -0
  106. data/test/support/logger_test.rb +31 -0
  107. data/test/support/template_test.rb +122 -0
  108. data/test/support/templater/erb.txt +2 -0
  109. data/test/support/templater/erb.yml +2 -0
  110. data/test/support/templater/somefile.txt +2 -0
  111. data/test/support/templater_test.rb +192 -0
  112. data/test/support/versions_test.rb +71 -0
  113. data/test/tap_test_helper.rb +4 -0
  114. data/test/tap_test_suite.rb +4 -0
  115. data/test/task/config/batch.yml +2 -0
  116. data/test/task/config/batched.yml +2 -0
  117. data/test/task/config/configured.yml +1 -0
  118. data/test/task/config/example.yml +1 -0
  119. data/test/task/config/overriding.yml +2 -0
  120. data/test/task/config/task_with_config.yml +1 -0
  121. data/test/task/config/template.yml +4 -0
  122. data/test/task_class_test.rb +118 -0
  123. data/test/task_execute_test.rb +233 -0
  124. data/test/task_test.rb +424 -0
  125. data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
  126. data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
  127. data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
  128. data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
  129. data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
  130. data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
  131. data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
  132. data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
  133. data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
  134. data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
  135. data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
  136. data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
  137. data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
  138. data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
  139. data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
  140. data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
  141. data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
  142. data/test/test/inference_methods_test.rb +311 -0
  143. data/test/test/subset_methods_test.rb +115 -0
  144. data/test/test_test.rb +233 -0
  145. data/test/workflow_test.rb +108 -0
  146. metadata +274 -0
@@ -0,0 +1,155 @@
1
+ require 'tap/support/template'
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Templater generates permutations of a hash, treating it as a template for other
7
+ # hashes. Templater looks for various flags on the end of keys to signify operations
8
+ # like method execution ('!') and replacement ('=').
9
+ #
10
+ # module TemplateMethods
11
+ # def method_call(input)
12
+ # self["result"] = "#{input} was processed"
13
+ # end
14
+ # end
15
+ #
16
+ # t = Templater.new(
17
+ # "key"=> "value",
18
+ # "method_call!" => "method input",
19
+ # "replacement=" => "replacement input")
20
+ # t.run_methods(TemplateMethods)
21
+ # t.run_replace do |input|
22
+ # "#{input} was replaced"
23
+ # end
24
+ #
25
+ # t.templates
26
+ # # => [{"key" => "value",
27
+ # "result" => "method input was processed",
28
+ # "replacement" => "replacement input was replaced"}]
29
+ #
30
+ # == Creating Template Methods
31
+ #
32
+ # The default Template methods may not be sufficient for your needs, but making new
33
+ # methods (as above) is easy. Some simple rules apply:
34
+ #
35
+ # - Any method can be called from run_methods provided it has a single input.
36
+ # - Modules with the methods will extend the hash templates so 'self' indicates the
37
+ # template itself.
38
+ # - New templates should be added by pushing the template to the templater, which
39
+ # will already be set by the time the method executes
40
+ # (ex: self.templater << new_template).
41
+ #
42
+ # Also, it's important to note that ANY empty templates at the end of run_methods
43
+ # are cleared from the templates array. Thus, if you want to remove a template from
44
+ # within a method, simply clear self.
45
+ #
46
+ # See the Template code for examples.
47
+ class Templater
48
+
49
+ # Convenience method for making a new Templater, running methods
50
+ # with the input modules, and making replacements using the input
51
+ # block. Returns the resulting templates.
52
+ def self.make_templates(hash, *modules, &block)
53
+ t = Templater.new(hash)
54
+ t.run_methods(*modules)
55
+ t.run_replace(&block)
56
+ t.templates
57
+ end
58
+
59
+ attr_reader :templates
60
+
61
+ # Creates a new Templater with the input hash as the initial template.
62
+ def initialize(hash)
63
+ @templates = []
64
+ self << hash
65
+ end
66
+
67
+ # Shortcut to add a template to the templater, unless the template is empty.
68
+ def <<(template)
69
+ self.templates << template unless template.empty?
70
+ end
71
+
72
+ # Iterates through the pairs in each template looking for keys matching the
73
+ # regexp (using =~). When a matching key is found, it is removed from the
74
+ # template and then the template, the key, and the corresponding value are
75
+ # passed to the block for further processing. Iteration restarts after each
76
+ # match and empty templates are removed upon completion. All templates are
77
+ # extended with Template before iteration.
78
+ #
79
+ # Notes:
80
+ # - run_templater is the foundation of the other run_* methods and may be used
81
+ # for custom template processing.
82
+ # - since =~ is used for comparison, only String keys will be selected
83
+ # under normal circumstances.
84
+ def run_templater(regexp) # :yields: template, key, value
85
+ templates.each do |template|
86
+ unless template.respond_to?(:templater)
87
+ template.extend Template
88
+ template.templater = self
89
+ end
90
+
91
+ template.each_pair do |key, value|
92
+ next unless key =~ regexp
93
+
94
+ # remove the key
95
+ template.delete(key)
96
+ yield(template, key, value)
97
+
98
+ # restart the loop as key/value pairs may have changed
99
+ retry
100
+ end
101
+ end
102
+
103
+ templates.delete_if {|t| t.empty?}
104
+
105
+ self
106
+ end
107
+
108
+ # Iterates through the pairs in each template looking for and executing method calls
109
+ # as per run_templater. Method calls are indicated by an exclamation attached to the
110
+ # key, like 'do_something!'.
111
+ #
112
+ # Templates are extended with the input modules before the method call. The modules
113
+ # should provide the methods (an error will be raised if the specified method is not
114
+ # defined). Note, templates will by default be extended with the Template module.
115
+ #
116
+ # module MethodModule
117
+ # def method_call(input)
118
+ # self["result"] = "#{input} was processed"
119
+ # end
120
+ # end
121
+ #
122
+ # t = Templater.new('method_call!' => 'input')
123
+ # t.run_methods(MethodModule) # calls 'do_something' with the input 'some value',
124
+ # # after removing the key-value pair
125
+ # t.templates # => [{"result" => "input was processed"}]
126
+ #
127
+ # Notes:
128
+ # - The method called is the method minus the exclamation, so 'do_something!' calls
129
+ # 'do_something' within the template and 'do_something!!' calls 'do_something!'
130
+ # - Iteration restarts after each method call; inserted method calls will be executed
131
+ def run_methods(*modules)
132
+ run_templater(/!$/) do |template, key, value|
133
+ modules.each {|mod| template.extend mod}
134
+ template.send(key.chop, value)
135
+ end
136
+ end
137
+
138
+ # Iterates through the pairs in each template looking for replacement keys and
139
+ # executing the block to make the appropriate replacement value, as per
140
+ # run_templater. Replacement keys are indicated by an equals sign attached to
141
+ # the key, like 'key='. The equals sign will be chopped off for these keys, and
142
+ # the value replaced with the return value of the block.
143
+ #
144
+ # t = Templater.new('replacement=' => 'some value')
145
+ # t.run_replace {|value| "#{value} was replaced"}
146
+ # t.templates # => [{"replacement" => "some value was replaced"}]
147
+ #
148
+ def run_replace # :yields: value
149
+ run_templater(/=$/) do |template, key, value|
150
+ template[key.chop] = yield(value)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,63 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # Version provides methods for adding, removing, and incrementing versions
5
+ # at the end of filepaths. Versions are all formatted like:
6
+ # 'filepath-version.extension'.
7
+ #
8
+ module Versions
9
+
10
+ # Adds a version to the filepath. Versioned filepaths follow the format:
11
+ # 'path-version.extension'. If no version is specified, then the filepath
12
+ # is returned.
13
+ #
14
+ # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
15
+ #
16
+ def version(path, version)
17
+ version = version.to_s.strip
18
+ if version.empty?
19
+ path
20
+ else
21
+ extname = File.extname(path)
22
+ path.chomp(extname) + '-' + version + extname
23
+ end
24
+ end
25
+
26
+ # Increments the version of the filepath by the specified increment.
27
+ #
28
+ # increment("path/to/file-1.0.txt", "0.0.1") # => "path/to/file-1.0.1.txt"
29
+ # increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
30
+ #
31
+ def increment(path, increment)
32
+ path, version = deversion(path)
33
+
34
+ # split the version and increment into integer arrays of equal length
35
+ increment, version = [increment, version].collect do |vstr|
36
+ begin
37
+ vstr.to_s.split(/\./).collect {|v| v.to_i}
38
+ rescue
39
+ raise "Bad version or increment: #{vstr}"
40
+ end
41
+ end
42
+ version.concat Array.new(increment.length - version.length, 0) if increment.length > version.length
43
+
44
+ # add the increment to version
45
+ 0.upto(version.length-1) do |i|
46
+ version[i] += (increment[i] || 0)
47
+ end
48
+
49
+ self.version(path, version.join("."))
50
+ end
51
+
52
+ # Splits the version from the input path, then returns the path and version.
53
+ # If no version is specified, then the returned version will be nil.
54
+ #
55
+ # deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
56
+ # deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
57
+ #
58
+ def deversion(path)
59
+ path =~ /^(.*)-(\d(\.?\d)*)(.*)?/ ? [$1 + $4, $2] : [path, nil]
60
+ end
61
+ end
62
+ end
63
+ end
data/lib/tap/task.rb ADDED
@@ -0,0 +1,448 @@
1
+ require 'monitor'
2
+
3
+ module Tap
4
+
5
+ # == Overview
6
+ #
7
+ # Tasks are the basic executable unit of Tap. When an App executes
8
+ # a Task, the Task will be passed an array of inputs which it then
9
+ # processes into outputs. Tasks are joined into workflows using
10
+ # conditions that define when a set of inputs are ready for execution,
11
+ # and what to do with the outputs when a the Task completes. Tasks
12
+ # therefore fundamentally consist of a condition_block, a process
13
+ # method, and an on_complete_block.
14
+ #
15
+ # Tasks are configurable. By default each task will be configured
16
+ # with the class default_config, which can be set when the class is
17
+ # defined.
18
+ #
19
+ # class ConfiguredTask < Tap::Task
20
+ # set_default_config :one => 'one', :two => 'two'
21
+ # end
22
+ #
23
+ # t = ConfiguredTask.new
24
+ # t.config # => {:one => 'one', :two => 'two'}
25
+ #
26
+ # Tasks also have a name (based on the class by default) which is
27
+ # used during initialization to lookup and merge any configurations
28
+ # in the corresponding config_file. Additional, overriding
29
+ # configurations may be provided during initialization as well.
30
+ #
31
+ # t.name # => "configured_task"
32
+ # t.app[:config] # => "/path/to/app/config"
33
+ # t.config_file # => "/path/to/app/config/configured_task.yml"
34
+ #
35
+ # # [/path/to/app/config/example.yml]
36
+ # # one: ONE
37
+ #
38
+ # t = ConfiguredTask.new "example", :three => 'three'
39
+ # t.name # => "example"
40
+ # t.config_file # => "/path/to/app/config/example.yml"
41
+ # t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
42
+ #
43
+ # === Batches
44
+ #
45
+ # Tasks are designed to facilitate batch processing of inputs. When
46
+ # a task queues inputs to itself, all inputs are collected into a
47
+ # single batch until execution at which time all inputs are passed
48
+ # to the task at once. If the task is iterative (the default), then
49
+ # each of these inputs is processed individually:
50
+ #
51
+ # runlist = []
52
+ # t1 = Task.new {|task, input| runlist << input}
53
+ # t1.queue 1
54
+ # t1.queue 2,3
55
+ # t1.app.run
56
+ #
57
+ # runlist # => [1,2,3]
58
+ #
59
+ # However, tasks are also designed to facilitate batch processing
60
+ # using a variety (ie batch) of configurations. When a task is
61
+ # queued, each task in the batch attribute will be queued as well.
62
+ #
63
+ # runlist = []
64
+ # t1.batch # => [t1]
65
+ # t2 = t1.create_batch_task
66
+ # t1.batch # => [t1, t2]
67
+ #
68
+ # t1.queue 1
69
+ # t2.queue 2,3
70
+ # t1.app.run
71
+ #
72
+ # runlist # => [1,2,3, 1,2,3]
73
+ #
74
+ # In the example, runlist reflects that t1 and t2 were run in succession
75
+ # with the same [1,2,3] inputs. Configuration files may specify an array
76
+ # of configuration templates; each is translated into a batched task.
77
+ #
78
+ # # [/path/to/app/config/batch.yml]
79
+ # # - one: ONE
80
+ # # - one: ANOTHER ONE
81
+ #
82
+ # t = ConfiguredTask.new "batch"
83
+ # t.batch.size # => 2
84
+ # t1, t2 = t.batch
85
+ #
86
+ # t1.name # => "batch"
87
+ # t1.config_template # => {:one => 'ONE'}
88
+ # t1.config # => {:one => 'ONE', :two => 'two'}
89
+ #
90
+ # t2.name # => "batch"
91
+ # t2.config_template # => {:one => 'ANOTHER ONE'}
92
+ # t2.config # => {:one => 'ANOTHER ONE', :two => 'two'}
93
+ #
94
+ # In addition, configuration files are processed to make the definition
95
+ # of configuration templates more concise and powerful
96
+ # (see Tap::Support::Templater for more details). The most basic form
97
+ # merges each variation with the rest of the file to produce the final
98
+ # array of templates.
99
+ #
100
+ # # [/path/to/app/config/template.yml]
101
+ # # one: ONE
102
+ # # variations!:
103
+ # # - two: TWO
104
+ # # - three: THREE
105
+ #
106
+ # t = ConfiguredTask.new "template"
107
+ # t.batch.size # => 2
108
+ # t1, t2 = t.batch
109
+ #
110
+ # t1.config # => {:one => 'ONE', :two => 'TWO'}
111
+ # t2.config # => {:one => 'ONE', :two => 'two', :three => 'THREE'}
112
+ #
113
+ # All together, these features make it easy to process a batch of inputs
114
+ # using a batch of configurations -- all encapsulated in a workflow-ready
115
+ # architecture.
116
+ class Task < Monitor
117
+ write_inheritable_hash(:default_config, {})
118
+ class_inheritable_hash(:default_config)
119
+
120
+ write_inheritable_attribute(:iterative, true)
121
+ class_inheritable_reader(:iterative)
122
+
123
+ class << self
124
+
125
+ # Sets the defualt configurations for the task class. Configurations
126
+ # are inherited, but can be overridden or added to in subclasses. Use
127
+ # options (:make_accessors, :make_readers, :make_writers) to create
128
+ # accessors for configurations.
129
+ #
130
+ # class SomeTask < Tap::Task
131
+ # set_default_config({
132
+ # :key => 'value'},
133
+ # :make_accessors => true)
134
+ # end
135
+ #
136
+ # t = SomeTask.new
137
+ # t.key # => 'value'
138
+ # t.key = 'another'
139
+ # t.config # => {:key => 'another'}
140
+ #
141
+ def set_default_config(hash={}, options={})
142
+ keys = hash.keys
143
+ self.write_inheritable_hash(:default_config, hash)
144
+
145
+ self.config_accessor(*keys) if options[:make_accessors]
146
+ self.config_reader(*keys) if options[:make_readers]
147
+ self.config_writer(*keys) if options[:make_writers]
148
+ end
149
+
150
+ # Creates a configuration writer for the input keys. Works like
151
+ # attr_writer, except the value is written to config, rather than
152
+ # a local variable.
153
+ def config_writer(*keys)
154
+ keys.each do |key|
155
+ define_method("#{key}=") do |value|
156
+ config[key] = value
157
+ end
158
+ end
159
+ end
160
+
161
+ # Creates a configuration reader for the input keys. Works like
162
+ # attr_reader, except the value is read from config, rather than
163
+ # a local variable.
164
+ def config_reader(*keys)
165
+ keys.each do |key|
166
+ define_method(key) do
167
+ config[key]
168
+ end
169
+ end
170
+ end
171
+
172
+ # Creates configuration accessors for the input keys. Works like
173
+ # attr_accessor, except the value is read from and written to config,
174
+ # rather than a local variable.
175
+ def config_accessor(*keys)
176
+ config_writer(*keys)
177
+ config_reader(*keys)
178
+ end
179
+
180
+ # Sets the default iteration behavior for a task class
181
+ # to iterate inputs during task execution.
182
+ def iterate
183
+ self.write_inheritable_attribute(:iterative, true)
184
+ end
185
+
186
+ # Sets the default iteration behavior for a task class
187
+ # to not iterate inputs during task execution.
188
+ def do_not_iterate
189
+ self.write_inheritable_attribute(:iterative, false)
190
+ end
191
+ end
192
+
193
+ attr_reader :name, :config, :config_template, :app, :batch, :condition_block, :task_block, :on_complete_block, :results
194
+ attr_accessor :multithread, :iterate
195
+
196
+ # Creates a new Task with the specified attributes.
197
+ def initialize(name=nil, config=nil, app=App.instance, &task_block)
198
+ super() # required for Monitor
199
+
200
+ self.name = name.nil? ? self.class.to_s.underscore : name
201
+ self.app = app
202
+
203
+ self.condition_block = nil
204
+ self.task_block = task_block
205
+ self.on_complete_block = nil
206
+ self.results = []
207
+ self.multithread = false
208
+ self.iterate = self.class.iterative
209
+ self.batch = []
210
+
211
+ # collect all configuration templates from the app
212
+ templates = []
213
+ app.config_templates(self.config_file).each do |template|
214
+ templates.concat make_config_templates(template)
215
+ end
216
+
217
+ # be sure there is at least one template and
218
+ # add a task to batch for every template
219
+ templates << {} if templates.empty?
220
+ templates.each do |template|
221
+ self.create_batch_task(template, config)
222
+ end
223
+ end
224
+
225
+ # Creates a batched task based on the config_template and overrides,
226
+ # and adds the task to batch. The batched task will be a duplicate
227
+ # of the current task but with new configurations.
228
+ #
229
+ # This method can be overridden to initialize variables that are
230
+ # specific to a batch task, for instance variables depending on
231
+ # batch_index.
232
+ def create_batch_task(config_template={}, overrides={})
233
+ task = (self.batch.empty? ? self : self.dup)
234
+ task.config_template = config_template.symbolize_keys
235
+ task.batch << task
236
+ task.config = overrides
237
+ task
238
+ end
239
+
240
+ # Returns true if the batch size is greater than one
241
+ # (the one being the current task itself).
242
+ def batched?
243
+ batch.length > 1
244
+ end
245
+
246
+ # Returns the index of the current task in batch.
247
+ def batch_index
248
+ batch.index(self)
249
+ end
250
+
251
+ # Returns true if multithread is true.
252
+ def multithread?
253
+ multithread
254
+ end
255
+
256
+ # Returns true if iterate is true. If iterate has not been set for the task,
257
+ # then iterate? returns the value of the class iterative variable.
258
+ def iterate?
259
+ iterate
260
+ end
261
+
262
+ # Returns the path to the configuration file used to make the task configuration
263
+ # templates. By default this is the YAML file for the task name relative to the
264
+ # application config directory:
265
+ #
266
+ # t.app['config'] # => '/path/to/config'
267
+ # t.name # => 'some/task'
268
+ # t.config_file # => '/path/to/config/some/task.yml'
269
+ #
270
+ def config_file
271
+ # since this is called during initialization before they
272
+ # are set, take care that this does not depend on batch,
273
+ # config, or config_template
274
+ app.filepath('config', self.name + ".yml")
275
+ end
276
+
277
+ # Configures the task with the given configuration overrides. A Task has three
278
+ # configuration sources: the class default_config, the config_template loaded from
279
+ # config_file, and the inputs to this method. Configurations from these
280
+ # sources are merged as: default_config.merge(config_template).merge(overrides)
281
+ #
282
+ # Configurations are symbolized before they are merged. If overrides is nil, then
283
+ # they will be treated as an empty hash.
284
+ def config=(overrides)
285
+ overrides = overrides.nil? ? {} : overrides.symbolize_keys
286
+ @config = self.class.default_config.merge(config_template).merge(overrides)
287
+
288
+ self.config
289
+ end
290
+
291
+ # Sets a condition block for the task. Raises an error if condition_block is
292
+ # already set, unless override = true.
293
+ def condition(override=false, &block) # :yields: self, inputs
294
+ raise "Condition for task already set: #{name}" unless condition_block.nil? || override
295
+ self.condition_block = block
296
+ end
297
+
298
+ # Sets a block to execute when the task completes execution. Raises an error
299
+ # if on_complete_block is already set, unless override = true.
300
+ def on_complete(override=false, &block) # :yields: results
301
+ raise "On complete for task already set: #{name}" unless on_complete_block.nil? || override
302
+ self.on_complete_block = block
303
+ end
304
+
305
+ # Returns true if no condition block is specified, or the condition
306
+ # block evaluates to true with the given inputs.
307
+ def executable?(inputs)
308
+ condition_block ? condition_block.call(self, inputs) : true
309
+ end
310
+
311
+ # Execute the actions associated with this task. Returns an array of Audits;
312
+ # one for each input value. Raises an error if the task is not executable with
313
+ # the inputs, and executes the on_complete block when finished.
314
+ #
315
+ # Execute is synchronized such that a task can only execute on one thread at a
316
+ # time.
317
+ def execute(*inputs)
318
+ synchronize do
319
+ self.results = nil
320
+ raise "Condition not met." unless executable?(inputs)
321
+
322
+ inputs = case
323
+ when inputs.empty? then inputs
324
+ when iterate? then Support::Audit.register(*inputs)
325
+ else [Support::Audit.merge(*inputs)]
326
+ end
327
+
328
+ before_execute
329
+ begin
330
+ self.results = inputs.each do |input|
331
+ output = process( input._current )
332
+ input._record(self, output)
333
+ end
334
+ rescue
335
+ on_execute_error($!)
336
+ end
337
+ after_execute
338
+
339
+ on_complete_block.call(results) if on_complete_block
340
+ results
341
+ end
342
+ end
343
+
344
+ # The method for processing inputs into outputs. Override this method in
345
+ # subclasses to provide class-specific process logic. By default the
346
+ # input is passed to the task_block (provided during initialization).
347
+ # Simply returns the input if no task_block is set.
348
+ def process(input)
349
+ task_block.nil? ? input : task_block.call(self, input)
350
+ end
351
+
352
+ # Enqueues self to the app queue with the inputs.
353
+ def enq(*inputs)
354
+ app.queue.enq(self, *inputs)
355
+ end
356
+
357
+ # Logs the inputs to the application logger (via app.log)
358
+ def log(action, msg="", level=Logger::INFO)
359
+ # TODO - add a task identifier?
360
+ app.log(action, msg, level)
361
+ end
362
+
363
+ protected
364
+
365
+ attr_writer :name, :config_template, :app, :batch, :condition_block, :task_block, :on_complete_block, :results
366
+
367
+ # Hook to execute code before inputs are processed.
368
+ def before_execute() end
369
+
370
+ # Hook to execute code after inputs are processed, but before the on_complete block.
371
+ def after_execute() end
372
+
373
+ # Hook to handle unhandled errors from processing inputs on a task level.
374
+ # By default on_execute_error simply re-raises the unhandled error.
375
+ def on_execute_error(err)
376
+ raise err
377
+ end
378
+
379
+ # Hook to provide class-specific processing of the config_file template
380
+ # (ie app.config_template(config_file)). By default the template is
381
+ # processed using Support::Templater.make_templates.
382
+ def make_config_templates(template)
383
+ Support::Templater.make_templates(template)
384
+ # this would be a security hole... can't do it.
385
+ # if you can set :data then you could read
386
+ # any accessible file.
387
+ #{ |filename| app.read(:data, filename) }
388
+ end
389
+
390
+ end
391
+ end
392
+
393
+ class Hold # :nodoc:
394
+ attr_accessor :help_doc
395
+
396
+ def register_doc(*section_headers)
397
+ section_headers << "Command Line Usage" if section_headers.empty?
398
+ self.help_doc = [caller.first.sub(/:\d+$/, ''), section_headers.collect {|section| section.strip}]
399
+ end
400
+
401
+ def help
402
+ return "No help available for #{self}" unless help_doc
403
+
404
+ lines = []
405
+ help_file, sections = help_doc
406
+ File.open(help_file) do |file|
407
+ in_section = nil
408
+ while line = file.gets
409
+ next unless line =~ /^#(.*)/
410
+ line = $1.strip
411
+
412
+ if line =~ /^=+(.*)/
413
+ line = $1.strip
414
+ in_section = sections.delete(line)
415
+ line += ":"
416
+ else
417
+ line = " " + line
418
+ end
419
+
420
+ lines << line if in_section
421
+ end
422
+ end
423
+
424
+ return lines.join("\n")
425
+ end
426
+
427
+ # Queues self to app with the input.
428
+ #def <<(input)
429
+ #queue(input)
430
+ #end
431
+
432
+ # Ticks the monitor by printing '.', or the specified message.
433
+ # Only ticks if called from within a monitor block.
434
+ #def tick_monitor(action=nil, msg="", level=Logger::INFO)
435
+ # app.tick_monitor(action, msg, level)
436
+ #end
437
+
438
+ # Prints the hash of statistics if called within a monitor block.
439
+ #def monitor_stats(hash)
440
+ # app.monitor_stats(hash)
441
+ #end
442
+
443
+ # Times the execution of the block and enables the various monitor
444
+ # methods (tick_monitor, monitor_stats).
445
+ # def monitor(action="beginning", msg="", &block)
446
+ # app.monitor(action,msg, &block)
447
+ #end
448
+ end