tap 0.8.0 → 0.9.0

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 (185) hide show
  1. data/Basic Overview +151 -0
  2. data/Command Reference +99 -0
  3. data/History +24 -0
  4. data/MIT-LICENSE +1 -1
  5. data/README +29 -57
  6. data/Rakefile +30 -37
  7. data/Tutorial +243 -191
  8. data/bin/tap +66 -35
  9. data/lib/tap.rb +47 -29
  10. data/lib/tap/app.rb +700 -342
  11. data/lib/tap/{script → cmd}/console.rb +0 -0
  12. data/lib/tap/{script → cmd}/destroy.rb +0 -0
  13. data/lib/tap/{script → cmd}/generate.rb +0 -0
  14. data/lib/tap/cmd/run.rb +156 -0
  15. data/lib/tap/constants.rb +4 -0
  16. data/lib/tap/dump.rb +57 -0
  17. data/lib/tap/env.rb +316 -0
  18. data/lib/tap/file_task.rb +106 -109
  19. data/lib/tap/generator.rb +4 -1
  20. data/lib/tap/generator/generators/command/USAGE +6 -0
  21. data/lib/tap/generator/generators/command/command_generator.rb +17 -0
  22. data/lib/tap/generator/generators/{script/templates/script.erb → command/templates/command.erb} +10 -10
  23. data/lib/tap/generator/generators/config/USAGE +21 -0
  24. data/lib/tap/generator/generators/config/config_generator.rb +17 -7
  25. data/lib/tap/generator/generators/file_task/USAGE +3 -0
  26. data/lib/tap/generator/generators/file_task/file_task_generator.rb +16 -0
  27. data/lib/tap/generator/generators/file_task/templates/file.txt +2 -0
  28. data/lib/tap/generator/generators/file_task/templates/file.yml +3 -0
  29. data/lib/tap/generator/generators/file_task/templates/task.erb +26 -20
  30. data/lib/tap/generator/generators/file_task/templates/test.erb +20 -10
  31. data/lib/tap/generator/generators/generator/generator_generator.rb +1 -1
  32. data/lib/tap/generator/generators/generator/templates/generator.erb +21 -12
  33. data/lib/tap/generator/generators/root/templates/Rakefile +33 -24
  34. data/lib/tap/generator/generators/root/templates/tap.yml +28 -31
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +1 -0
  36. data/lib/tap/generator/generators/task/USAGE +3 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +18 -5
  38. data/lib/tap/generator/generators/task/templates/task.erb +7 -12
  39. data/lib/tap/generator/generators/task/templates/test.erb +10 -11
  40. data/lib/tap/generator/generators/workflow/templates/task.erb +1 -1
  41. data/lib/tap/generator/generators/workflow/templates/test.erb +1 -1
  42. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  43. data/lib/tap/patches/rake/testtask.rb +55 -0
  44. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  45. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  46. data/lib/tap/root.rb +172 -67
  47. data/lib/tap/script.rb +70 -336
  48. data/lib/tap/support/aggregator.rb +55 -0
  49. data/lib/tap/support/audit.rb +281 -280
  50. data/lib/tap/support/batchable.rb +59 -0
  51. data/lib/tap/support/class_configuration.rb +279 -0
  52. data/lib/tap/support/configurable.rb +92 -0
  53. data/lib/tap/support/configurable_methods.rb +296 -0
  54. data/lib/tap/support/executable.rb +98 -0
  55. data/lib/tap/support/executable_queue.rb +82 -0
  56. data/lib/tap/support/logger.rb +9 -15
  57. data/lib/tap/support/rake.rb +43 -54
  58. data/lib/tap/support/run_error.rb +32 -13
  59. data/lib/tap/support/shell_utils.rb +47 -0
  60. data/lib/tap/support/tdoc.rb +9 -8
  61. data/lib/tap/support/tdoc/config_attr.rb +40 -16
  62. data/lib/tap/support/validation.rb +77 -0
  63. data/lib/tap/support/versions.rb +36 -36
  64. data/lib/tap/task.rb +276 -482
  65. data/lib/tap/test.rb +20 -261
  66. data/lib/tap/test/env_vars.rb +7 -5
  67. data/lib/tap/test/file_methods.rb +126 -121
  68. data/lib/tap/test/subset_methods.rb +86 -45
  69. data/lib/tap/test/tap_methods.rb +271 -0
  70. data/lib/tap/workflow.rb +174 -46
  71. data/test/app/config/another/task.yml +1 -0
  72. data/test/app/config/erb.yml +2 -1
  73. data/test/app/config/some/task.yml +1 -0
  74. data/test/app/config/template.yml +2 -6
  75. data/test/app_test.rb +1241 -1008
  76. data/test/env/test_configure/recurse_a.yml +2 -0
  77. data/test/env/test_configure/recurse_b.yml +2 -0
  78. data/test/env/test_configure/tap.yml +23 -0
  79. data/test/env/test_load_env_config/dir/tap.yml +3 -0
  80. data/test/env/test_load_env_config/recurse_a.yml +2 -0
  81. data/test/env/test_load_env_config/recurse_b.yml +2 -0
  82. data/test/env/test_load_env_config/tap.yml +3 -0
  83. data/test/env_test.rb +198 -0
  84. data/test/file_task_test.rb +70 -53
  85. data/{lib/tap/generator/generators/package/USAGE → test/root/file.txt} +0 -0
  86. data/test/root_test.rb +621 -454
  87. data/test/script_test.rb +38 -174
  88. data/test/support/aggregator_test.rb +99 -0
  89. data/test/support/audit_test.rb +409 -416
  90. data/test/support/batchable_test.rb +74 -0
  91. data/test/support/{task_configuration_test.rb → class_configuration_test.rb} +106 -47
  92. data/test/{task/config/overriding.yml → support/configurable/config/configured.yml} +0 -0
  93. data/test/support/configurable_test.rb +295 -0
  94. data/test/support/executable_queue_test.rb +103 -0
  95. data/test/support/executable_test.rb +38 -0
  96. data/test/support/logger_test.rb +17 -17
  97. data/test/support/rake_test.rb +4 -2
  98. data/test/support/shell_utils_test.rb +24 -0
  99. data/test/support/tdoc_test.rb +265 -258
  100. data/test/support/validation_test.rb +54 -0
  101. data/test/support/versions_test.rb +38 -38
  102. data/test/tap_test_helper.rb +19 -5
  103. data/test/tap_test_suite.rb +5 -2
  104. data/test/task_base_test.rb +13 -104
  105. data/test/task_syntax_test.rb +300 -0
  106. data/test/task_test.rb +258 -381
  107. data/test/test/env_vars_test.rb +40 -40
  108. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/expected/one.txt +0 -0
  109. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/expected/two.txt +0 -0
  110. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/input/one.txt +0 -0
  111. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/input/two.txt +0 -0
  112. data/test/test/{test_file_task_test → file_methods/test_assert_files_can_have_no_expected_files_if_specified}/input/one.txt +0 -0
  113. data/test/test/{test_file_task_test → file_methods/test_assert_files_can_have_no_expected_files_if_specified}/input/two.txt +0 -0
  114. data/test/test/file_methods/test_assert_files_fails_for_different_content/expected/one.txt +1 -0
  115. data/test/test/{test_file_task_test → file_methods/test_assert_files_fails_for_different_content}/expected/two.txt +0 -0
  116. data/test/test/file_methods/test_assert_files_fails_for_different_content/input/one.txt +1 -0
  117. data/test/test/file_methods/test_assert_files_fails_for_different_content/input/two.txt +1 -0
  118. data/test/test/{test_file_task_test → file_methods/test_assert_files_fails_for_missing_expected_file}/expected/one.txt +0 -0
  119. data/test/test/file_methods/test_assert_files_fails_for_missing_expected_file/input/one.txt +1 -0
  120. data/test/test/file_methods/test_assert_files_fails_for_missing_expected_file/input/two.txt +1 -0
  121. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/expected/one.txt +1 -0
  122. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/expected/two.txt +1 -0
  123. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/input/one.txt +1 -0
  124. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/input/two.txt +1 -0
  125. data/test/test/file_methods/test_assert_files_fails_for_no_expected_files/input/one.txt +1 -0
  126. data/test/test/file_methods/test_assert_files_fails_for_no_expected_files/input/two.txt +1 -0
  127. data/test/test/file_methods_doc/test_sub/expected/one.txt +1 -0
  128. data/test/test/file_methods_doc/test_sub/expected/two.txt +1 -0
  129. data/test/test/file_methods_doc/test_sub/input/one.txt +1 -0
  130. data/test/test/file_methods_doc/test_sub/input/two.txt +1 -0
  131. data/test/test/file_methods_doc_test.rb +29 -0
  132. data/test/test/file_methods_test.rb +214 -143
  133. data/test/test/subset_methods_test.rb +111 -115
  134. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/expected/task/name/a.txt +0 -0
  135. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/expected/task/name/b.txt +0 -0
  136. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/input/a.txt +0 -0
  137. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/input/b.txt +0 -0
  138. data/test/test/tap_methods_test.rb +399 -0
  139. data/test/workflow_test.rb +101 -91
  140. metadata +86 -70
  141. data/lib/tap/generator/generators/package/package_generator.rb +0 -38
  142. data/lib/tap/generator/generators/package/templates/package.erb +0 -186
  143. data/lib/tap/generator/generators/script/USAGE +0 -0
  144. data/lib/tap/generator/generators/script/script_generator.rb +0 -17
  145. data/lib/tap/script/run.rb +0 -154
  146. data/lib/tap/support/batch_queue.rb +0 -162
  147. data/lib/tap/support/combinator.rb +0 -114
  148. data/lib/tap/support/task_configuration.rb +0 -169
  149. data/lib/tap/support/template.rb +0 -81
  150. data/lib/tap/support/templater.rb +0 -155
  151. data/lib/tap/version.rb +0 -4
  152. data/test/app/config/addition_template.yml +0 -6
  153. data/test/app_class_test.rb +0 -33
  154. data/test/check/binding_eval.rb +0 -23
  155. data/test/check/define_method_check.rb +0 -22
  156. data/test/check/dependencies_check.rb +0 -175
  157. data/test/check/inheritance_check.rb +0 -22
  158. data/test/support/batch_queue_test.rb +0 -320
  159. data/test/support/combinator_test.rb +0 -249
  160. data/test/support/template_test.rb +0 -122
  161. data/test/support/templater/erb.txt +0 -2
  162. data/test/support/templater/erb.yml +0 -2
  163. data/test/support/templater/somefile.txt +0 -2
  164. data/test/support/templater_test.rb +0 -192
  165. data/test/task/config/template.yml +0 -4
  166. data/test/task_class_test.rb +0 -170
  167. data/test/task_execute_test.rb +0 -262
  168. data/test/test/file_methods/test_assert_expected/expected/file.txt +0 -1
  169. data/test/test/file_methods/test_assert_expected/expected/folder/file.txt +0 -1
  170. data/test/test/file_methods/test_assert_expected/input/file.txt +0 -1
  171. data/test/test/file_methods/test_assert_expected/input/folder/file.txt +0 -1
  172. data/test/test/file_methods/test_assert_files_exist/input/input_1.txt +0 -0
  173. data/test/test/file_methods/test_assert_files_exist/input/input_2.txt +0 -0
  174. data/test/test/file_methods/test_file_compare/expected/output_1.txt +0 -3
  175. data/test/test/file_methods/test_file_compare/expected/output_2.txt +0 -1
  176. data/test/test/file_methods/test_file_compare/input/input_1.txt +0 -3
  177. data/test/test/file_methods/test_file_compare/input/input_2.txt +0 -3
  178. data/test/test/file_methods/test_infer_glob/expected/file.yml +0 -0
  179. data/test/test/file_methods/test_infer_glob/expected/file_1.txt +0 -0
  180. data/test/test/file_methods/test_infer_glob/expected/file_2.txt +0 -0
  181. data/test/test/file_methods/test_yml_compare/expected/output_1.yml +0 -6
  182. data/test/test/file_methods/test_yml_compare/expected/output_2.yml +0 -6
  183. data/test/test/file_methods/test_yml_compare/input/input_1.yml +0 -4
  184. data/test/test/file_methods/test_yml_compare/input/input_2.yml +0 -4
  185. data/test/test_test.rb +0 -373
@@ -12,7 +12,7 @@
12
12
  # unaffected as the constants are reset after RDoc loads.
13
13
  #
14
14
  if Object.const_defined?(:RubyToken) || Object.const_defined?(:RubyLex)
15
- class Object # :nodoc:
15
+ class Object
16
16
  old_ruby_token = const_defined?(:RubyToken) ? remove_const(:RubyToken) : nil
17
17
  old_ruby_lex = const_defined?(:RubyLex) ? remove_const(:RubyLex) : nil
18
18
 
@@ -34,13 +34,13 @@ else
34
34
  require 'rdoc/rdoc'
35
35
 
36
36
  if Object.const_defined?(:RubyToken) && !RDoc.const_defined?(:RubyToken)
37
- class Object # :nodoc:
37
+ class Object
38
38
  RDoc.const_set(:RubyToken, remove_const(:RubyToken))
39
39
  end
40
40
  end
41
41
 
42
42
  if Object.const_defined?(:RubyLex) && !RDoc.const_defined?(:RubyLex)
43
- class Object # :nodoc:
43
+ class Object
44
44
  RDoc.const_set(:RubyLex, remove_const(:RubyLex))
45
45
  RDoc::RubyLex.const_set(:RubyLex, RDoc::RubyLex)
46
46
  end
@@ -113,8 +113,8 @@ module Tap
113
113
  lines = []
114
114
  else
115
115
  if normalize_comments
116
- line =~ /^\s*#(.*)/
117
- line = $1.to_s.strip
116
+ line =~ /^\s*#\s?(.*)/
117
+ line = $1.to_s
118
118
  end
119
119
 
120
120
  lines << line
@@ -129,7 +129,8 @@ module Tap
129
129
  def find_class_or_module_named(name)
130
130
  return self if full_name == name
131
131
  (@classes.values + @modules.values).each do |c|
132
- return c if c.find_class_or_module_named(name)
132
+ res = c.find_class_or_module_named(name)
133
+ return res if res
133
134
  end
134
135
  nil
135
136
  end
@@ -206,7 +207,25 @@ module Tap
206
207
  # (see 'rdoc/parsers/parse_rb' line 2509)
207
208
  def parse_config(context, single, tk, comment)
208
209
  tks = get_tk_to_nl
210
+
211
+ key_tk = nil
212
+ value_tk = nil
213
+
214
+ tks.each do |token|
215
+ next if token.kind_of?(TkSPACE)
209
216
 
217
+ if key_tk == nil
218
+ key_tk = token if token.kind_of?(TkSYMBOL)
219
+ else
220
+ case token
221
+ when TkCOMMA then value_tk = token
222
+ else
223
+ value_tk = token if value_tk.kind_of?(TkCOMMA)
224
+ break
225
+ end
226
+ end
227
+ end
228
+
210
229
  text = ""
211
230
  if tks.last.kind_of?(TkCOMMENT)
212
231
  text = tks.last.text.chomp("\n").chomp("\r")
@@ -216,18 +235,23 @@ module Tap
216
235
 
217
236
  tmp = RDoc::CodeObject.new
218
237
  read_documentation_modifiers(tmp, RDoc::ATTR_MODIFIERS)
219
- return unless tmp.document_self
238
+ text = nil unless tmp.document_self
220
239
  end
221
-
222
- key_tk = tks.select {|tk| tk.kind_of?(TkSYMBOL)}.first
223
- return if key_tk.nil?
240
+
241
+ tks.reverse_each {|token| unget_tk(token) }
242
+ return if key_tk == nil || text == nil
224
243
 
225
244
  arg = key_tk.text[1..-1]
245
+ if value_tk
246
+ if text =~ /(.*):no_default:(.*)/
247
+ text = $1 + $2
248
+ else
249
+ text += " (#{value_tk.text})"
250
+ end
251
+ end
226
252
  att = TDoc::ConfigAttr.new(text, arg, config_rw, comment)
227
253
  att.config_declaration = get_tkread
228
-
229
- # TODO -- it would be nice to read the default value here...
230
-
254
+
231
255
  context.add_attribute(att)
232
256
  end
233
257
 
@@ -241,14 +265,14 @@ module Tap
241
265
  self.config_mode = tk.name
242
266
 
243
267
  tks = get_tk_to_nl
244
- is_config_mode_flag = tks.select do |tk|
245
- !tk.kind_of?(TkSPACE) && !tk.kind_of?(TkCOMMENT)
268
+ is_config_mode_flag = tks.select do |token|
269
+ !token.kind_of?(TkSPACE) && !token.kind_of?(TkCOMMENT)
246
270
  end.empty?
247
271
 
248
272
  # If no args are given, take this as a flag for c
249
273
  return if is_config_mode_flag
250
274
 
251
- tks.reverse_each {|tk| unget_tk(tk) }
275
+ tks.reverse_each {|token| unget_tk(token) }
252
276
  args = parse_symbol_arg
253
277
  read = get_tkread
254
278
  rw = "?"
@@ -0,0 +1,77 @@
1
+ autoload(:PP, 'pp')
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Validation generates blocks for common validations/processing of
7
+ # configurations set through Configurable. These blocks can be passed
8
+ # to the config declarations using an ampersand (&).
9
+ #
10
+ # See the 'Configuration' section in the Tap::Task documentation for
11
+ # more details on how Validation works in practice.
12
+ module Validation
13
+
14
+ # Raised when Validation blocks fail.
15
+ class ValidationError < ArgumentError
16
+ def initialize(input, validations)
17
+ validation_str = PP.singleline_pp(validations, "")
18
+ super PP.singleline_pp(input, "expected #{validation_str} but was: ")
19
+ end
20
+ end
21
+
22
+ module_function
23
+
24
+ # Yaml conversion and checker. Valid if any of the validations
25
+ # match in a case statement. Otherwise raises an error.
26
+
27
+ # Returns input if any of the validations match the input, as
28
+ # in a case statement. Raises a ValidationError otherwise.
29
+ #
30
+ # For example:
31
+ #
32
+ # validate(10, [Integer, nil])
33
+ #
34
+ # Does the same as:
35
+ #
36
+ # case 10
37
+ # when Integer, nil then input
38
+ # else raise ValidationError.new(...)
39
+ # end
40
+ #
41
+ def validate(input, validations)
42
+ case input
43
+ when *validations then input
44
+ else
45
+ raise ValidationError.new(input, validations)
46
+ end
47
+ end
48
+
49
+ # Returns a block that calls validate using the block input
50
+ # and the input validations.
51
+ def check(*validations)
52
+ lambda {|input| validate(input, validations) }
53
+ end
54
+
55
+ # Returns a block that loads input strings as YAML, then
56
+ # calls validate with the result and the input validations.
57
+ # If the block input is not a string, the block input is
58
+ # validated.
59
+ #
60
+ # b = yaml(Integer, nil)
61
+ # b.class # => Proc
62
+ # b.call(1) # => 1
63
+ # b.call("1") # => 1
64
+ # b.call(nil) # => nil
65
+ # b.call("str") # => ValidationError
66
+ #
67
+ # Note: yaml is especially useful for validating configs
68
+ # that may be specified as strings or as an actual object.
69
+ def yaml(*validations)
70
+ lambda do |input|
71
+ res = input.kind_of?(String) ? YAML.load(input) : input
72
+ validate(res, validations)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -11,7 +11,7 @@ module Tap
11
11
  # 'path-version.extension'. If no version is specified, then the filepath
12
12
  # is returned.
13
13
  #
14
- # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
14
+ # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
15
15
  #
16
16
  def version(path, version)
17
17
  version = version.to_s.strip
@@ -26,7 +26,7 @@ module Tap
26
26
  # Increments the version of the filepath by the specified increment.
27
27
  #
28
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"
29
+ # increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
30
30
  #
31
31
  def increment(path, increment)
32
32
  path, version = deversion(path)
@@ -52,43 +52,43 @@ module Tap
52
52
  # Splits the version from the input path, then returns the path and version.
53
53
  # If no version is specified, then the returned version will be nil.
54
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]
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
57
  #
58
58
  def deversion(path)
59
59
  path =~ /^(.*)-(\d(\.?\d)*)(.*)?/ ? [$1 + $4, $2] : [path, nil]
60
- end
61
-
62
- # A <=> comparison for versions. compare_versions can take strings,
63
- # integers, or even arrays representing the parts of a version.
64
- #
65
- # compare_versions("1.0.0", "0.9.9") # => 1
66
- # compare_versions(1.1, 1.1) # => 0
67
- # compare_versions([0,9], [0,9,1]) # => -1
68
- def compare_versions(a,b)
69
- a, b = [a,b].collect {|item| to_integer_array(item) }
70
-
71
- # equalize the lengths of the integer arrays
72
- d = b.length - a.length
73
- case
74
- when d < 0 then b.concat Array.new(-d, 0)
75
- when d > 0 then a.concat Array.new(d, 0)
76
- end
77
-
78
- a <=> b
79
- end
80
-
81
- private
82
-
83
- # Converts an input argument (typically a string or an array)
84
- # to an array of integers. Splits version string on "."
85
- def to_integer_array(arg)
86
- arr = case arg
87
- when Array then arg
88
- else arg.to_s.split('.')
89
- end
90
- arr.collect {|i| i.to_i}
91
- end
60
+ end
61
+
62
+ # A <=> comparison for versions. compare_versions can take strings,
63
+ # integers, or even arrays representing the parts of a version.
64
+ #
65
+ # compare_versions("1.0.0", "0.9.9") # => 1
66
+ # compare_versions(1.1, 1.1) # => 0
67
+ # compare_versions([0,9], [0,9,1]) # => -1
68
+ def compare_versions(a,b)
69
+ a, b = [a,b].collect {|item| to_integer_array(item) }
70
+
71
+ # equalize the lengths of the integer arrays
72
+ d = b.length - a.length
73
+ case
74
+ when d < 0 then b.concat Array.new(-d, 0)
75
+ when d > 0 then a.concat Array.new(d, 0)
76
+ end
77
+
78
+ a <=> b
79
+ end
80
+
81
+ private
82
+
83
+ # Converts an input argument (typically a string or an array)
84
+ # to an array of integers. Splits version string on "."
85
+ def to_integer_array(arg)
86
+ arr = case arg
87
+ when Array then arg
88
+ else arg.to_s.split('.')
89
+ end
90
+ arr.collect {|i| i.to_i}
91
+ end
92
92
 
93
93
  end
94
94
  end
@@ -1,14 +1,58 @@
1
1
  module Tap
2
-
3
- # == Overview
4
- #
5
- # Tasks are the basic executable unit of Tap. When an App executes
6
- # a Task, the Task will be passed inputs which it then processes into
7
- # results. Tasks are joined into workflows using condition blocks
8
- # defining when a set of inputs are ready for execution, and where to
9
- # pass results when the Task completes. Tasks fundamentally consist
10
- # of a condition_block, a process method, and an on_complete_block.
11
- #
2
+ # = Overview
3
+ #
4
+ # Tasks are the basic organizational unit of Tap. Tasks provide
5
+ # a standard backbone for creating the working parts of an application
6
+ # by facilitating configuration, batched execution of methods, and
7
+ # interaction with the command line.
8
+ #
9
+ # The functionality of Task is built from several base modules:
10
+ # - Tap::Support::Batchable
11
+ # - Tap::Support::Configurable
12
+ # - Tap::Support::Executable
13
+ #
14
+ # Tap::Workflow is built on the same foundations; the sectons on
15
+ # configuration and batching apply equally to Workflows as Tasks.
16
+ #
17
+ # === Task Definition
18
+ #
19
+ # Tasks are instantiated with a task block; when the task is run
20
+ # the block gets called with the enqued inputs. As such, the block
21
+ # should specify the same number of inputs as you enque (plus the
22
+ # task itself, which is a standard input).
23
+ #
24
+ # no_inputs = Task.new {|task| }
25
+ # one_input = Task.new {|task, input| }
26
+ # mixed_inputs = Task.new {|task, a, b, *args| }
27
+ #
28
+ # no_inputs.enq
29
+ # one_input.enq(:a)
30
+ # mixed_inputs.enq(:a, :b)
31
+ # mixed_inputs.enq(:a, :b, 1, 2, 3)
32
+ #
33
+ # Subclasses of Task specify executable code by overridding the process
34
+ # method. In this case the number of enqued inputs should correspond to
35
+ # process (passing the task would be redundant).
36
+ #
37
+ # class NoInput < Tap::Task
38
+ # def process() end
39
+ # end
40
+ #
41
+ # class OneInput < Tap::Task
42
+ # def process(input) end
43
+ # end
44
+ #
45
+ # class MixedInputs < Tap::Task
46
+ # def process(a, b, *args) end
47
+ # end
48
+ #
49
+ # NoInput.new.enq
50
+ # OneInput.new.enq(:a)
51
+ # MixedInputs.new.enq(:a, :b)
52
+ # MixedInputs.new.enq(:a, :b, 1, 2, 3)
53
+ #
54
+ # === Configuration
55
+ #
12
56
  # Tasks are configurable. By default each task will be configured
13
57
  # with the default class configurations, which can be set when the
14
58
  # class is defined.
@@ -19,516 +63,262 @@ module Tap
19
63
  # end
20
64
  #
21
65
  # t = ConfiguredTask.new
22
- # t.config # => {:one => 'one', :two => 'two'}
23
- #
24
- # Tasks also have a name (based on the class name by default). The
25
- # task name is used as a relative filepath from application directories
26
- # to assoicated files. During initialization, the task name is used
27
- # to lookup configurations from config_file. Additional and overriding
28
- # configurations may be provided during initialization.
29
- #
30
- # t.name # => "configured_task"
31
- # t.app[:config] # => "/path/to/app/config"
32
- # t.config_file # => "/path/to/app/config/configured_task.yml"
33
- #
34
- # # [/path/to/app/config/example.yml]
35
- # # one: ONE
36
- #
37
- # t = ConfiguredTask.new "example", :three => 'three'
38
- # t.name # => "example"
39
- # t.config_file # => "/path/to/app/config/example.yml"
40
- # t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
66
+ # t.name # => "configured_task"
67
+ # t.config # => {:one => 'one', :two => 'two'}
41
68
  #
42
- # === Batches
69
+ # Configurations can be validated or processed using an optional
70
+ # block. Tap::Support::Validation pre-packages several common
71
+ # validation/processing blocks, and can be accessed through the
72
+ # class method 'c':
43
73
  #
44
- # Tasks are designed to facilitate batch processing of inputs. Each
45
- # time a task queues inputs to itself, the inputs are collected into a
46
- # batch. Upon execution, all inputs are passed to the task at once.
47
- # If the task is iterative (the default), then each of these inputs is
48
- # processed individually. Otherwise, all inputs are processed as a single
49
- # group:
74
+ # class ValidatingTask < Tap::Task
75
+ # # string config validated to be a string
76
+ # config :string, 'str', &c.check(String)
50
77
  #
51
- # runlist = []
52
- # t1 = Task.new {|task, input| runlist << input}
53
- # t1.enq 1
54
- # t1.enq 2,3
55
- # t1.app.run
56
- # runlist # => [1,2,3]
78
+ # # integer config; string inputs are converted using YAML
79
+ # config :integer, 1, &c.yaml(Integer)
80
+ # end
57
81
  #
58
- # runlist = []
59
- # t1.iterate = false
60
- # t1.enq 1
61
- # t1.enq 2,3
62
- # t1.app.run
63
- # runlist # => [[1,2,3]]
82
+ # t = ValidatingTask.new
83
+ # t.string = 1 # => ValidationError
84
+ # t.integer = 1.1 # => ValidationError
64
85
  #
65
- # Additionally, tasks are designed to facilitate processing using a batch
66
- # of related tasks. Often these batches consist of the same task class,
67
- # but with a variety of configurations, but they can be assembled in other
68
- # ways. When a task is queued, each task in the batch will be queued:
86
+ # t.integer = "1"
87
+ # t.integer == 1 # => true
69
88
  #
70
- # runlist = []
71
- # t1 = Task.new {|task, input| runlist << input}
72
- # t1.batch # => [t1]
73
- # t2 = t1.create_batch_task
74
- # t1.batch # => [t1, t2]
75
- #
76
- # t1.enq 1
77
- # t2.enq 2,3
78
- # t1.app.run
79
- # runlist # => [1,2,3, 1,2,3]
80
- #
81
- # Here runlist reflects that t1 and t2 were run in succession with the same [1,2,3]
82
- # inputs. Configuration batches are automatically generated when task.config_file
83
- # specifies an array of configurations; each configuration is translated into a
84
- # batched task.
89
+ # Tasks have a name that gets used as a relative filepath to find
90
+ # associated files (for instance config_file). By default the task
91
+ # name is based on the task class, such that Tap::Task corresponds
92
+ # to 'tap/task'. Custom names can be provided when a task is
93
+ # initialized, as can additional and/or overriding configurations.
94
+ #
95
+ # # [/path/to/app/config/example.yml]
96
+ # # one: ONE
97
+ #
98
+ # t = ConfiguredTask.new "example", :three => 'three'
99
+ # t.name # => "example"
100
+ # t.app[:config] # => "/path/to/app/config"
101
+ # t.config_file # => "/path/to/app/config/example.yml"
102
+ # t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
103
+ #
104
+ # Tasks can be assembled into batches that enque and execute
105
+ # collectively. Batched tasks are automatically generated when a
106
+ # config_file specifies an array of configurations.
85
107
  #
86
108
  # # [/path/to/app/config/batch.yml]
87
109
  # # - one: ONE
88
110
  # # - one: ANOTHER ONE
89
111
  #
90
112
  # t = ConfiguredTask.new "batch"
91
- # t.batch.size # => 2
113
+ # t.batch.size # => 2
92
114
  # t1, t2 = t.batch
93
115
  #
94
- # t1.name # => "batch"
95
- # t1.config_template # => {:one => 'ONE'}
96
- # t1.config # => {:one => 'ONE', :two => 'two'}
116
+ # t1.name # => "batch"
117
+ # t1.config # => {:one => 'ONE', :two => 'two'}
97
118
  #
98
- # t2.name # => "batch"
99
- # t2.config_template # => {:one => 'ANOTHER ONE'}
100
- # t2.config # => {:one => 'ANOTHER ONE', :two => 'two'}
119
+ # t2.name # => "batch"
120
+ # t2.config # => {:one => 'ANOTHER ONE', :two => 'two'}
101
121
  #
102
- # Task processes configuration files to make the definition of configuration
103
- # templates more DRY and powerful (see Tap::Support::Templater for more
104
- # details). Here the default ConfiguredTask configurations are overridden
105
- # by variations created by the variations! tag:
106
- #
107
- # # [/path/to/app/config/template.yml]
108
- # # one: ONE
109
- # # variations!:
110
- # # - two: TWO
111
- # # - three: THREE
122
+ # === Batches
112
123
  #
113
- # t = ConfiguredTask.new "template"
114
- # t.batch.size # => 2
115
- # t1, t2 = t.batch
124
+ # Tasks facilitate batch processing of inputs using batched tasks. Often
125
+ # a batch consists of the the same task class instantiated with a variety
126
+ # of configurations. Once batched, tasks enque together; when any one of
127
+ # the tasks is enqued, the entire batch is enqued.
116
128
  #
117
- # t1.config # => {:one => 'ONE', :two => 'TWO'}
118
- # t2.config # => {:one => 'ONE', :two => 'two', :three => 'THREE'}
129
+ # runlist = []
130
+ # t1 = Task.new {|task, input| runlist << input}
131
+ # t1.batch # => [t1]
119
132
  #
120
- # All together, these features make it easy to process a batch of inputs
121
- # using a batch of configurations -- all encapsulated in a workflow-ready
122
- # architecture.
133
+ # t2 = t1.initialize_batch_obj
134
+ # t1.batch # => [t1, t2]
135
+ # t2.batch # => [t1, t2]
136
+ #
137
+ # t1.enq 1
138
+ # t2.enq 2
139
+ # t1.app.run
123
140
  #
124
- # == Non-Task Tasks!
141
+ # runlist # => [1,1,2,2]
125
142
  #
126
- # The methods definining the essential behavior of a Task are encapsulated in
127
- # the Tap::Task::Base module. Using this module, Non-Task classes can be defined,
128
- # and Non-Task objects extended to behave like Tasks; ie they can be enqued, run,
129
- # and incorporated into workflows.
143
+ # Here runlist reflects that t1 and t2 were run in succession with the 1
144
+ # input, and then the 2 input.
130
145
  #
131
- # See Tap::Task::Base for more details (esp for objects with a process method),
132
- # as well as Tap::Support::Rake, which makes Rake tasks behave like Tap tasks.
146
+ # === Non-Task Tasks
133
147
  #
134
- class Task < Monitor
135
- write_inheritable_attribute(:configurations, Support::TaskConfiguration.new)
136
- class_inheritable_reader(:configurations)
137
-
138
- write_inheritable_attribute(:iterative, true)
139
- class_inheritable_reader(:iterative)
140
-
141
- write_inheritable_attribute(:source_files, [])
142
- class_inheritable_reader(:source_files)
143
-
144
- class << self
145
-
146
- # Currently an experimental way of identifying source files for TDoc
147
- # documentation.
148
- def source_file(arg) # :nodoc:
149
- source_files << arg
150
- end
151
-
152
- # Declares a configuration without any accessors.
153
- #
154
- # With no keys specified, sets config to make no accessors
155
- # for each new configuration.
156
- def declare_config(*keys)
157
- if keys.empty?
158
- self.config_mode = :none
159
- else
160
- keys.each do |key|
161
- configurations.declare(key, self)
162
- end
163
- end
164
- end
165
-
166
- # Creates a configuration writer for the input keys. Works like
167
- # attr_writer, except the value is written to config, rather than
168
- # a local variable. In addition, the config will be validated
169
- # using validate_config upon setting the value.
170
- #
171
- # With no keys specified, sets config to create config_writer
172
- # for each new configuration.
173
- def config_writer(*keys)
174
- if keys.empty?
175
- self.config_mode = :config_writer
176
- else
177
- keys.each do |key|
178
- configurations.declare(key, self)
179
- define_config_writer(key)
180
- end
181
- end
182
- end
183
-
184
- # Creates a configuration reader for the input keys. Works like
185
- # attr_reader, except the value is read from config, rather than
186
- # a local variable.
187
- #
188
- # With no keys specified, sets config to create a config_reader
189
- # for each new configuration.
190
- def config_reader(*keys)
191
- if keys.empty?
192
- self.config_mode = :config_reader
193
- else
194
- keys.each do |key|
195
- configurations.declare(key, self)
196
- define_config_reader(key)
197
- end
198
- end
199
- end
200
-
201
- # Creates configuration accessors for the input keys. Works like
202
- # attr_accessor, except the value is read from and written to config,
203
- # rather than a local variable.
204
- #
205
- # With no keys specified, sets config to create a config_accessor
206
- # for each new configuration.
207
- def config_accessor(*keys)
208
- if keys.empty?
209
- self.config_mode = :config_accessor
210
- else
211
- keys.each do |key|
212
- configurations.declare(key, self)
213
- define_config_reader(key)
214
- define_config_writer(key)
215
- end
216
- end
217
- end
148
+ # The essential behavior of a Task is expressed in the Tap::Task::Base
149
+ # module. Using this module, non-task classes can be made to behave like
150
+ # tasks. An even more fundamental module, Tap::Executable, allows any
151
+ # method to behave in this manner.
152
+ #
153
+ # Configurations are specific to Task but batches are not. Non-task
154
+ # tasks can be batched. Executable methods cannot be batched.
155
+ #
156
+ # See Tap::Task::Base and Tap::Support::Executable for more details as
157
+ # well as Tap::Support::Rake::Task, which makes Rake[http://rake.rubyforge.org/]
158
+ # tasks behave like Tap tasks.
159
+ class Task
160
+
161
+ # Defines the essential behavior of a Task. Using this module,
162
+ # non-task classes can be made to behave like tasks; ie they can
163
+ # be enqued, batched, and incorporated into workflows.
164
+ module Base
165
+ include Support::Executable
218
166
 
219
- # Sets a class configuration. Configurations are inherited, but can
220
- # be overridden or added in subclasses. Accessors are created by
221
- # default, but this behavior can be modified by use of the other
222
- # config methods.
223
- #
224
- # class SampleTask < Tap::Task
225
- # config :key, 'value'
226
- #
227
- # config_reader
228
- # config :reader_only
229
- # end
230
- #
231
- # t = SampleTask.new
232
- # t.respond_to?(:reader_only) # => true
233
- # t.respond_to?(:reader_only=) # => false
234
- #
235
- # t.config # => {:key => 'value', :reader_only => nil}
236
- # t.key # => 'value'
237
- # t.key = 'another'
238
- # t.config # => {:key => 'another', :reader_only => nil}
239
- def config(key, value=nil, attributes={})
240
- declare_config(key)
241
- configurations.set(key, value, attributes)
242
-
243
- case config_mode
244
- when nil, :config_accessor then config_accessor(key)
245
- when :config_writer then config_writer(key)
246
- when :config_reader then config_reader(key)
247
- end
248
- end
167
+ attr_reader :app
249
168
 
250
- # Sets the default iteration behavior for a task class
251
- # to iterate inputs during task execution. As a result,
252
- # tasks will NOT execute without inputs.
253
- def iterate
254
- self.write_inheritable_attribute(:iterative, true)
169
+ # Initializes obj to behave like a Task. The input method will be
170
+ # called when obj is run by Tap::App.
171
+ def self.initialize(obj, method_name, app=App.instance)
172
+ obj.extend Base
173
+ obj.extend Support::Batchable
174
+ obj.instance_variable_set(:@app, app)
175
+ obj.instance_variable_set(:@batch, [])
176
+ obj.instance_variable_set(:@multithread, false)
177
+ obj.instance_variable_set(:@on_complete_block, nil)
178
+ obj.instance_variable_set(:@_method_name, method_name)
179
+ obj.initialize_batch_obj
180
+ obj
255
181
  end
256
182
 
257
- # Sets the default iteration behavior for a task class
258
- # to not iterate inputs during task execution. As a result,
259
- # tasks can execute without inputs.
260
- def do_not_iterate
261
- self.write_inheritable_attribute(:iterative, false)
183
+ # Enqueues self and self.batch to app with the inputs.
184
+ # The number of inputs provided should match the number
185
+ # of inputs specified by the arity of the _method_name method.
186
+ def enq(*inputs)
187
+ batch.each {|t| t.unbatched_enq(*inputs) }
188
+ self
262
189
  end
263
-
264
- protected
265
-
266
- attr_accessor :config_mode
267
190
 
268
- private
269
-
270
- def define_config_reader(key) # :nodoc:
271
- define_method(key) do
272
- config[key]
273
- end
191
+ # Like enq, but only enques self and not self.batch.
192
+ def unbatched_enq(*inputs)
193
+ app.queue.enq(self, inputs)
274
194
  end
275
195
 
276
- def define_config_writer(key) # :nodoc:
277
- define_method("#{key}=") do |value|
278
- config[key] = value
279
- validate_config(key, value)
280
- value
281
- end
282
- end
283
- end
284
-
285
- # Tap::Task::Base encapsulates the methods definining the essential
286
- # behavior of a Task.
287
- module Base
288
- attr_reader :app, :batch, :condition_block, :on_complete_block, :results
289
- attr_accessor :multithread, :iterate
290
-
291
- # Creates a new Task with the specified attributes.
292
- def self.extended(base)
293
- base.extend MonitorMixin
196
+ alias :unbatched_on_complete :on_complete
294
197
 
295
- base.instance_variable_set("@app", App.instance) if base.app.nil?
296
- base.instance_variable_set("@batch", [base]) if base.batch.nil?
297
- base.instance_variable_set("@results", []) if base.results.nil?
298
- base.instance_variable_set("@condition_block", nil) if base.condition_block.nil?
299
- base.instance_variable_set("@on_complete_block", nil) if base.on_complete_block.nil?
300
- base.instance_variable_set("@multithread", false) if base.multithread.nil?
301
- base.instance_variable_set("@iterate", false) if base.iterate.nil?
198
+ # Sets the on_complete_block for self and self.batch.
199
+ # Use unbatched_on_complete to set the on_complete_block
200
+ # for just self.
201
+ def on_complete(override=false, &block)
202
+ batch.each {|t| t.unbatched_on_complete(override, &block)}
203
+ self
302
204
  end
303
205
 
304
- # Returns true if the batch size is greater than one
305
- # (the one being the current task itself).
306
- def batched?
307
- batch.length > 1
308
- end
309
-
310
- # Returns the index of the current task in batch.
311
- def batch_index
312
- batch.index(self)
313
- end
314
-
315
- # Returns true if multithread is true.
316
- #
317
- # (NOTE -- multithreading is currently experimental!)
318
- def multithread?
319
- multithread
320
- end
321
-
322
- # Returns true if iterate is true. If iterate has not been set for the task,
323
- # then iterate? returns the value of the class iterative variable.
324
- def iterate?
325
- iterate
326
- end
327
-
328
- # Sets a condition block for the task. Raises an error if condition_block is
329
- # already set, unless override = true.
330
- #
331
- # Note that the block will recieve the audited_inputs of the task
332
- # (see Audit for more information).
333
- def condition(override=false, &block) # :yields: self, audited_inputs
334
- raise "Condition for task already set: #{self}" unless condition_block.nil? || override
335
- self.condition_block = block
336
- end
337
-
338
- # Sets a block to execute when the task completes execution. Raises an error
339
- # if on_complete_block is already set, unless override = true.
340
- #
341
- # Note that the block will recieve the results of the task, ie an array of
342
- # audited values and not values themselves (see Audit for more information).
343
- def on_complete(override=false, &block) # :yields: results
344
- raise "On complete for task already set: #{self}" unless on_complete_block.nil? || override
345
- self.on_complete_block = block
346
- end
347
-
348
- # Returns true if no condition block is specified, or the condition
349
- # block evaluates to true with the given inputs.
350
- def executable?(inputs)
351
- condition_block ? condition_block.call(self, inputs) : true
352
- end
353
-
354
- # Execute the actions associated with this task. Returns an array of Audits;
355
- # one for each input value. Raises an error if the task is not executable with
356
- # the inputs, and executes the on_complete block when finished.
357
- #
358
- # Execute is synchronized such that a task can only execute on one thread at a
359
- # time.
360
- def execute(*inputs)
361
- synchronize do
362
- check_terminate
363
-
364
- audited_inputs = if iterate?
365
- Support::Audit.register(*inputs)
366
- else
367
- Support::Audit.merge(*inputs)
368
- end
369
-
370
- self.results = nil
371
- raise "Condition not met." unless executable?(audited_inputs)
372
-
373
- before_execute
374
- begin
375
- self.results = if iterate?
376
- audited_inputs.collect do |audited_input|
377
- on_execute(audited_input)
378
- end
379
- else
380
- [on_execute(audited_inputs)]
381
- end
382
- rescue
383
- on_execute_error($!)
384
- end
385
- after_execute
386
-
387
- on_complete_block.call(results) if on_complete_block
388
- results
389
- end
206
+ alias :unbatched_multithread= :multithread=
207
+
208
+ # Sets the multithread for self and self.batch. Use
209
+ # unbatched_multithread= to set multithread for just self.
210
+ def multithread=(value)
211
+ batch.each {|t| t.unbatched_multithread = value }
212
+ self
390
213
  end
391
214
 
392
- # Raises a TerminateError if the application is in state
393
- # TERMINATE, as a mechanism to gracefully terminate threads.
394
- #--
395
- # Only occurs if the task is multithreaded.
396
- #++
397
- #
398
- # check_terminate is called before each execution, but can
399
- # be manually called at any time to provide a breakpoint
400
- # in long executions.
215
+ # Raises a TerminateError if app.state == State::TERMINATE.
216
+ # check_terminate may be called at any time to provide a
217
+ # breakpoint in long-running processes.
401
218
  def check_terminate
402
- #if multithread? && app.state == App::State::TERMINATE
403
219
  if app.state == App::State::TERMINATE
404
220
  raise App::TerminateError.new
405
221
  end
406
- end
407
-
408
- protected
409
-
410
- attr_writer :app, :batch, :condition_block, :on_complete_block, :results
411
-
412
- # Hook to execute code before inputs are processed.
413
- def before_execute() end
414
-
415
- # Hook for overridding the execution code that generates the
416
- # task results. The input will be an audited version of the
417
- # inputs array provided to execute, or, if iterate? is true,
418
- # an audited version of each individual input provided to
419
- # execute. on_execute should return the audited_inputs.
420
- #
421
- # By default, on_execute sends the current value of the audited
422
- # input process and then records the output with self as the source.
423
- #
424
- # It is not advised to override the default on_execute unless
425
- # absolutely necessary. Consider adjusting the behavior of
426
- # process first.
427
- def on_execute(audited_inputs)
428
- output = process(audited_inputs._current)
429
- audited_inputs._record(self, output)
430
- end
431
-
432
- # Hook to execute code after inputs are processed, but before the on_complete block.
433
- def after_execute() end
434
-
435
- # Hook to handle unhandled errors from processing inputs on a task level.
436
- # By default on_execute_error simply re-raises the unhandled error.
437
- def on_execute_error(err)
438
- raise err
439
- end
222
+ end
440
223
  end
441
224
 
442
225
  include Base
443
-
444
- attr_reader :name, :config, :config_template, :task_block
445
-
446
- # Creates a new Task with the specified attributes.
447
- def initialize(name=nil, config=nil, app=App.instance, &task_block)
448
- super() # required for Monitor
449
-
450
- self.name = name.nil? ? self.class.to_s.underscore : name
451
- self.app = app
452
-
453
- self.condition_block = nil
454
- self.task_block = task_block
455
- self.on_complete_block = nil
456
- self.results = []
457
- self.multithread = false
458
- self.iterate = self.class.iterative
459
- self.batch = []
460
-
461
- # collect all configuration templates from the app
462
- templates = []
463
- app.config_templates(self.config_file).each do |template|
464
- templates.concat make_config_templates(template)
465
- end
466
-
467
- # be sure there is at least one template and
468
- # add a task to batch for every template
469
- templates << {} if templates.empty?
470
- templates.each do |template|
471
- self.create_batch_task(template, config)
472
- end
473
- end
226
+ include Support::Configurable
474
227
 
475
- # Creates a batched task based on the config_template and overrides,
476
- # and adds the task to batch. The batched task will be a duplicate
477
- # of the current task but with new configurations.
478
- #
479
- # This method can be overridden to initialize variables that are
480
- # specific to a batch task, for instance variables depending on
481
- # batch_index.
482
- def create_batch_task(config_template={}, overrides={})
483
- task = (self.batch.empty? ? self : self.dup)
484
- task.config_template = config_template.symbolize_keys
485
- task.batch << task
486
- task.config = overrides
487
- task
488
- end
228
+ attr_reader :task_block
489
229
 
490
- # Returns the path to the configuration file used to make the task configuration
491
- # templates. By default this is the YAML file for the task name relative to the
492
- # application config directory:
230
+ # Creates a new Task with the specified attributes.
493
231
  #
494
- # t.app['config'] # => '/path/to/config'
495
- # t.name # => 'some/task'
496
- # t.config_file # => '/path/to/config/some/task.yml'
232
+ # === Subclassing
233
+ # Batched tasks are generated by duplicating an existing instance, hence
234
+ # it is a good idea to set shared instance variables BEFORE calling super
235
+ # in a subclass initialize method. Non-shared instance variables can be
236
+ # set by overriding the initialize_batch_obj method:
497
237
  #
498
- def config_file
499
- # since this is called during initialization before they
500
- # are set, take care that this does not depend on batch,
501
- # config, or config_template
502
- app.filepath('config', self.name + ".yml")
503
- end
504
-
505
- # Configures the task with the given configuration overrides. A Task has three
506
- # configuration sources: the default class configurations, the config_template
507
- # loaded from config_file, and the inputs to this method. Configurations from these
508
- # sources are merged as: default_config.merge(config_template).merge(overrides)
238
+ # class SubclassTask < Tap::Task
239
+ # attr_accessor :shared_variable, :instance_specific_variable
240
+ #
241
+ # def initialize(*args)
242
+ # @shared_variable = Object.new
243
+ # super
244
+ # end
245
+ #
246
+ # def initialize_batch_obj(*args)
247
+ # task = super
248
+ # task.instance_specific_variable = Object.new
249
+ # task
250
+ # end
251
+ # end
509
252
  #
510
- # Configurations are symbolized before they are merged. If overrides is nil, then
511
- # they will be treated as an empty hash. All configurations are validated using
512
- # validate_config.
513
- def config=(overrides)
514
- overrides = overrides.nil? ? {} : overrides.symbolize_keys
515
- @config = self.class.configurations.default.merge(config_template).merge(overrides)
516
- self.config.each_pair {|key, value| validate_config(key, value) }
517
- self.config
253
+ # t1 = SubclassTask.new
254
+ # t2 = t1.initialize_batch_obj
255
+ # t1.shared_variable == t2.shared_variable # => true
256
+ # t1.instance_specific_variable == t2.instance_specific_variable # => false
257
+ #
258
+ def initialize(name=nil, config={}, app=App.instance, &task_block)
259
+ @task_block = (task_block == nil ? default_task_block : task_block)
260
+ @multithread = false
261
+ @on_complete_block = nil
262
+ @_method_name = :execute
263
+ super(name, config, app)
518
264
  end
519
265
 
520
- # The method for processing inputs into outputs. Override this method in
521
- # subclasses to provide class-specific process logic. By default the
522
- # input is passed to the task_block (provided during initialization).
523
- # Simply returns the input if no task_block is set.
524
- def process(input)
525
- check_terminate
526
- task_block.nil? ? input : task_block.call(self, input)
266
+ # Executes self with the given inputs. Execute provides hooks for subclasses
267
+ # to insert standard execution code: before_execute, on_execute_error,
268
+ # and after_execute. Override any/all of these methods as needed.
269
+ #
270
+ # Execute passes the inputs to process and returns the result.
271
+ def execute(*inputs)
272
+ before_execute
273
+ begin
274
+ result = process(*inputs)
275
+ rescue
276
+ on_execute_error($!)
277
+ end
278
+ after_execute
279
+
280
+ result
527
281
  end
528
282
 
529
- # Enqueues self to the app queue with the inputs.
530
- def enq(*inputs)
531
- app.queue.enq(self, *inputs)
283
+ # The method for processing inputs into outputs. Override this method in
284
+ # subclasses to provide class-specific process logic. The number of
285
+ # arguments specified by process corresponds to the number of arguments
286
+ # the task should have when enqued.
287
+ #
288
+ # class TaskWithTwoInputs < Tap::Task
289
+ # def process(a, b)
290
+ # [b,a]
291
+ # end
292
+ # end
293
+ #
294
+ # t = TaskWithTwoInputs.new
295
+ # t.enq(1,2).enq(3,4)
296
+ # t.app.run
297
+ # t.app.results(t) # => [[2,1], [4,3]]
298
+ #
299
+ # By default process passes self and the input(s) to the task_block
300
+ # provided during initialization. In this case the task block dictates
301
+ # the number of arguments enq should receive. Simply returns the inputs
302
+ # if no task_block is set.
303
+ #
304
+ # # two arguments in addition to task are specified
305
+ # # so this Task must be enqued with two inputs...
306
+ # t = Task.new {|task, a, b| [b,a] }
307
+ # t.enq(1,2).enq(3,4)
308
+ # t.app.run
309
+ # t.app.results(t) # => [[2,1], [4,3]]
310
+ #
311
+ def process(*inputs)
312
+ return inputs if task_block == nil
313
+ inputs.unshift(self)
314
+
315
+ arity = task_block.arity
316
+ n = inputs.length
317
+ unless n == arity || (arity < 0 && (-1-n) <= arity)
318
+ raise ArgumentError.new("wrong number of arguments (#{n} for #{arity})")
319
+ end
320
+
321
+ task_block.call(*inputs)
532
322
  end
533
323
 
534
324
  # Logs the inputs to the application logger (via app.log)
@@ -537,24 +327,28 @@ module Tap
537
327
  app.log(action, msg, level)
538
328
  end
539
329
 
330
+ # Returns self.name
331
+ def to_s
332
+ name
333
+ end
334
+
540
335
  protected
541
-
542
- attr_writer :name, :config_template, :task_block
543
336
 
544
- # Hook to validate configurations.
545
- def validate_config(key, value)
337
+ # Hook to set a default task block. By default, nil.
338
+ def default_task_block
339
+ nil
546
340
  end
547
341
 
548
- # Hook to provide class-specific processing of the config_file template
549
- # (ie app.config_template(config_file)). By default the template is
550
- # processed using Support::Templater.make_templates.
551
- def make_config_templates(template)
552
- Support::Templater.make_templates(template)
553
- # this would be a security hole... can't do it.
554
- # if you can set :data then you could read
555
- # any accessible file.
556
- #{ |filename| app.read(:data, filename) }
557
- end
342
+ # Hook to execute code before inputs are processed.
343
+ def before_execute() end
344
+
345
+ # Hook to execute code after inputs are processed.
346
+ def after_execute() end
558
347
 
348
+ # Hook to handle unhandled errors from processing inputs on a task level.
349
+ # By default on_execute_error simply re-raises the unhandled error.
350
+ def on_execute_error(err)
351
+ raise err
352
+ end
559
353
  end
560
354
  end