tap 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
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