bahuvrihi-tap 0.10.2 → 0.10.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ require 'rack'
2
+ require 'cgi'
3
+
4
+ module Tap
5
+ module Support
6
+ module Gems
7
+
8
+ Tap::Env.manifest(:public_paths, "public") do |search_path|
9
+ Dir.glob(File.join(search_path, "**/*")).collect do |path|
10
+ ["/" + Tap::Root.relative_filepath(search_path, path), path]
11
+ end
12
+ end
13
+
14
+ Tap::Env.manifest(:templates, "template") do |search_path|
15
+ Dir.glob(File.join(search_path, "**/*.erb")).collect do |path|
16
+ ["/" + Tap::Root.relative_filepath(search_path, path), path]
17
+ end
18
+ end
19
+
20
+ Tap::Env.manifest(:cgis, "cgi") do |search_path|
21
+ Dir.glob(File.join(search_path, "**/*.rb")).collect do |path|
22
+ ["/" + Tap::Root.relative_filepath(search_path, path), path]
23
+ end
24
+ end
25
+
26
+ module Rack
27
+ def call(env)
28
+ path = env['PATH_INFO']
29
+
30
+ case
31
+ when public_page = search(:public_paths, path)
32
+ # serve named static pages
33
+ response(env) { File.read(public_page) }
34
+
35
+ when cgi_page = search(:cgis, path)
36
+ # serve cgis relative to a cgi path
37
+ run_cgi(cgi_page, env)
38
+
39
+ when path == "/" || path == "/index"
40
+ # serve up the homepage
41
+ if env["QUERY_STRING"] == "refresh=true"
42
+ reset(:cgis) do |key, path|
43
+ Support::Lazydoc[path].resolved = false
44
+ end
45
+ end
46
+ template_response('index', env)
47
+
48
+ else
49
+ # handle all other requests as errors
50
+ template_response('404', env)
51
+
52
+ end
53
+ end
54
+
55
+ #--
56
+ # Runs a cgi and returns an array as demanded by rack.
57
+ def run_cgi(cgi_path, env)
58
+ current_output = $>
59
+ cgi_output = StringIO.new("")
60
+
61
+ begin
62
+ $> = cgi_output
63
+
64
+ with_env(env) { load(cgi_path) }
65
+
66
+ # collect the headers and body from the cgi output
67
+ headers, body = cgi_output.string.split(/\r?\n\r?\n/, 2)
68
+
69
+ raise "missing headers from: #{cgi_path}" if headers == nil
70
+ body = "" if body == nil
71
+
72
+ headers = headers.split(/\r?\n/).inject({}) do |hash, line|
73
+ key, value = line.split(/:/, 2)
74
+ hash[key] = value
75
+ hash
76
+ end
77
+
78
+ [headers.delete('Status') || 200, headers, body]
79
+ rescue(Exception)
80
+ # when an error occurs, return a standard cgi error with backtrace
81
+ [500, {'Content-Type' => 'text/plain'}, %Q{#{$!.class}: #{$!.message}\n#{$!.backtrace.join("\n")}}]
82
+ ensure
83
+ $> = current_output
84
+ end
85
+ end
86
+
87
+ # Executes block with ENV set to the specified hash. Non-string env variables are not set.
88
+ def with_env(env)
89
+ current_env = {}
90
+ ENV.each_pair {|key, value| current_env[key] = value }
91
+
92
+ begin
93
+ ENV.clear
94
+ env.each_pair {|key, value| ENV[key] = value if value.kind_of?(String)}
95
+
96
+ yield
97
+ ensure
98
+ ENV.clear
99
+ current_env.each_pair {|key, value| ENV[key] = value }
100
+ end
101
+ end
102
+
103
+ DEFAULT_ERROR_TEMPLATE = %Q{
104
+ <html>
105
+ <body>
106
+ # Error handling request: <%= error.message %></br>
107
+ # <%= error.backtrace.join("<br/># ") %>
108
+
109
+ <code><pre>
110
+ <%= cgi.to_yaml %>
111
+ <%= rack.to_yaml %>
112
+ </pre></code>
113
+ </body>
114
+ </html>
115
+ }
116
+
117
+ def response(env)
118
+ ::Rack::Response.new.finish do |res|
119
+ res.write begin
120
+ yield(res)
121
+ rescue
122
+ template(DEFAULT_ERROR_TEMPLATE, env, :error => $!)
123
+ end
124
+ end
125
+ end
126
+
127
+ def template(template, env, attributes={})
128
+ # partition and sort the env variables into
129
+ # cgi and rack variables.
130
+ rack, cgi = env.to_a.partition do |(key, value)|
131
+ key =~ /^rack/
132
+ end.collect do |part|
133
+ part.sort_by do |key, value|
134
+ key
135
+ end.inject({}) do |hash, (key,value)|
136
+ hash[key] = value
137
+ hash
138
+ end
139
+ end
140
+
141
+ Templater.new( template , {:server => self, :env => env, :cgi => cgi, :rack => rack}.merge(attributes) ).build
142
+ end
143
+
144
+ def template_response(name, env)
145
+ path = search(:templates, name)
146
+ response(env) { template(File.read(path), env) }
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,81 @@
1
+ module Tap
2
+ module Support
3
+ module Parsers
4
+ class Base
5
+ class << self
6
+ # Parses the input string as YAML, if the string matches the YAML document
7
+ # specifier (ie it begins with "---\s*\n"). Otherwise returns the string.
8
+ #
9
+ # str = {'key' => 'value'}.to_yaml # => "--- \nkey: value\n"
10
+ # Tap::Script.parse_yaml(str) # => {'key' => 'value'}
11
+ # Tap::Script.parse_yaml("str") # => "str"
12
+ def parse_yaml(str)
13
+ str =~ /\A---\s*\n/ ? YAML.load(str) : str
14
+ end
15
+ end
16
+
17
+ attr_reader :argvs
18
+ attr_reader :rounds
19
+ attr_reader :sequences
20
+ attr_reader :forks
21
+ attr_reader :merges
22
+ attr_reader :sync_merges
23
+
24
+ def build(env, app)
25
+ # attempt lookup and instantiate the task class
26
+ task_declarations = argvs.collect do |argv|
27
+ pattern = argv.shift
28
+
29
+ const = env.search(:tasks, pattern) or raise ArgumentError, "unknown task: #{pattern}"
30
+ task_class = const.constantize or raise ArgumentError, "unknown task: #{pattern}"
31
+ task_class.instantiate(argv, app)
32
+ end
33
+
34
+ # remove tasks used by the workflow
35
+ tasks = targets.collect do |index|
36
+ task, args = task_declarations[index]
37
+
38
+ unless args.empty?
39
+ raise ArgumentError, "workflow target receives args: #{task} [#{args.join(', ')}]"
40
+ end
41
+
42
+ tasks[index] = nil
43
+ task
44
+ end
45
+
46
+ # build the workflow
47
+ [:sequence, :fork, :merge, :sync_merge].each do |type|
48
+ send("#{type}s").each do |source, targets|
49
+ source.send(type, *targets.collect {|t| tasks[t] })
50
+ end
51
+ end
52
+
53
+ # build queues
54
+ queues = rounds.collect do |round|
55
+ round.each do |index|
56
+ task, args = task_declarations[index]
57
+ task.enq(*args) if task
58
+ end
59
+
60
+ app.queue.clear
61
+ end
62
+ queues.delete_if {|queue| queue.empty? }
63
+
64
+ queues
65
+ end
66
+
67
+ protected
68
+
69
+ def targets
70
+ results = sequences.collect {|source, targets| targets } +
71
+ forks.collect {|source, targets| targets } +
72
+ merges.collect {|target, sources| target } +
73
+ sync_merges.collect {|target, sources| target }
74
+
75
+ results.flatten.uniq.sort
76
+ end
77
+
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,90 @@
1
+ require 'tap/support/parsers/base'
2
+
3
+ module Tap
4
+ module Support
5
+ module Parsers
6
+ class CommandLine < Base
7
+ class << self
8
+ def parse_sequence(str, count=0)
9
+ seq = []
10
+ seq << count if str[0] == ?:
11
+ str.split(/:+/).each do |n|
12
+ seq << n.to_i unless n.empty?
13
+ end
14
+ seq << count + 1 if str[-1] == ?:
15
+ [seq.shift, seq]
16
+ end
17
+
18
+ def pairs_regexp(l, r)
19
+ /\A--(\d*)#{Regexp.escape(l)}([\d,]*)#{Regexp.escape(r)}\z/
20
+ end
21
+
22
+ def parse_pairs(lead, str, count=0)
23
+ bracket = []
24
+ str.split(/,+/).each do |n|
25
+ bracket << n.to_i unless n.empty?
26
+ end
27
+
28
+ [lead.empty? ? count : lead.to_i, bracket]
29
+ end
30
+ end
31
+
32
+ ROUND = /\A--(\+(\d+)|\+*)\z/
33
+ SEQUENCE = /\A--(\d*(:\d*)+)\z/
34
+ FORK = pairs_regexp("[", "]")
35
+ MERGE = pairs_regexp("{", "}")
36
+ SYNC_MERGE = pairs_regexp("(", ")")
37
+ INVALID = /\A--(\z|[^A-Za-z])/
38
+
39
+ def initialize(argv)
40
+ @sequences = []
41
+ @forks = []
42
+ @merges = []
43
+ @sync_merges = []
44
+
45
+ current = []
46
+ current_round = []
47
+ @argvs = []
48
+ @rounds = [current_round]
49
+
50
+ argv.each do |arg|
51
+ unless arg =~ INVALID
52
+ current << arg
53
+ next
54
+ end
55
+
56
+ # for peformance split to match
57
+ # most arguments just once.
58
+ unless current.empty?
59
+ current_round << @argvs.length
60
+ @argvs << current
61
+ current = []
62
+ end
63
+
64
+ case arg
65
+ when ROUND
66
+ current_round = (@rounds[$2 ? $2.to_i : $1.length] ||= [])
67
+ when SEQUENCE
68
+ @sequences << CommandLine.parse_sequence($1, @argvs.length-1)
69
+ when FORK
70
+ @forks << CommandLine.parse_pairs($1, $2, @argvs.length-1)
71
+ when MERGE
72
+ @merges << CommandLine.parse_pairs($1, $2, @argvs.length-1)
73
+ when SYNC_MERGE
74
+ @sync_merges << CommandLine.parse_pairs($1, $2, @argvs.length-1)
75
+ else
76
+ raise ArgumentError, "invalid argument: #{arg}"
77
+ end
78
+ end
79
+
80
+ unless current.empty?
81
+ current_round << @argvs.length
82
+ @argvs << current
83
+ end
84
+ @rounds.delete_if {|round| round.nil? || round.empty? }
85
+ end
86
+
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,83 @@
1
+ require 'tap/support/parsers/base'
2
+
3
+ module Tap
4
+ module Support
5
+ module Parsers
6
+
7
+ # rounds syntax
8
+ # 0[task]=dump&0[config][key]=value&0[input][]=a&0[input][]=b
9
+ # sequence[1]=1,2,3
10
+ # fork[1]=2,3
11
+ # round[0]=1,2,3
12
+ # ....
13
+
14
+ class Server < Base
15
+
16
+ class << self
17
+ def parse_argv(hash)
18
+ raise ArgumentError, "no task specified" unless hash.has_key?('task')
19
+
20
+ # parse task
21
+ argv = [hash['task']]
22
+
23
+ # parse configs
24
+ configs = hash['config']
25
+ configs = YAML.load(configs) if configs.kind_of?(String)
26
+
27
+ case configs
28
+ when Hash
29
+ configs.each_pair do |key, value|
30
+ argv << "--#{key}"
31
+ argv << value
32
+ end
33
+ when nil
34
+ else raise ArgumentError, "non-hash configs specified: #{configs}"
35
+ end
36
+
37
+ # parse inputs
38
+ inputs = hash['inputs']
39
+ inputs = YAML.load(inputs) if inputs.kind_of?(String)
40
+
41
+ case inputs
42
+ when Array then argv.concat(inputs)
43
+ when nil
44
+ else raise ArgumentError, "non-array inputs specified: #{inputs}"
45
+ end
46
+
47
+ argv
48
+ end
49
+
50
+ def parse_pairs(values)
51
+ [*values].collect do |value|
52
+ value.split(',').collect {|i| i.to_i }
53
+ end.collect do |split|
54
+ next if split.empty?
55
+ [split.shift, split]
56
+ end.compact
57
+ end
58
+ end
59
+
60
+ INDEX = /\A\d+\z/
61
+
62
+ def initialize(argh)
63
+ @argvs = []
64
+
65
+ argh.each_pair do |key, value|
66
+ case key
67
+ when INDEX
68
+ argvs[key.to_i] = Server.parse_argv(value)
69
+ else
70
+ instance_variable_set("@#{key}s", Server.parse_pairs(value))
71
+ end
72
+ end
73
+
74
+ @rounds ||= []
75
+ @sequences ||= []
76
+ @forks ||= []
77
+ @merges ||= []
78
+ @sync_merges ||= []
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/tap/task.rb CHANGED
@@ -1,4 +1,6 @@
1
- require 'tap/support/framework'
1
+ require 'tap/support/batchable'
2
+ require 'tap/support/executable'
3
+ require 'tap/support/command_line'
2
4
 
3
5
  module Tap
4
6
 
@@ -154,19 +156,309 @@ module Tap
154
156
  # t1.array == t2.array # => true
155
157
  # t1.array.object_id == t2.array.object_id # => false
156
158
  #
157
- class Task
159
+ class Task
160
+ include Support::Batchable
161
+ include Support::Configurable
158
162
  include Support::Executable
159
- include Support::Framework
160
163
 
161
- attr_reader :task_block
164
+ class << self
165
+ # Returns the default name for the class: to_s.underscore
166
+ attr_accessor :default_name
167
+
168
+ # Returns class dependencies
169
+ attr_reader :dependencies
170
+
171
+ def inherited(child)
172
+ unless child.instance_variable_defined?(:@source_file)
173
+ caller.first =~ Support::Lazydoc::CALLER_REGEXP
174
+ child.instance_variable_set(:@source_file, File.expand_path($1))
175
+ end
176
+
177
+ child.instance_variable_set(:@default_name, child.to_s.underscore)
178
+ child.instance_variable_set(:@dependencies, dependencies.dup)
179
+ super
180
+ end
181
+
182
+ def instance
183
+ @instance ||= new
184
+ end
185
+
186
+ # Generates or updates the specified subclass of self.
187
+ def subclass(const_name, configs={}, dependencies=[], options={}, &block)
188
+ #
189
+ # Lookup or create the subclass constant.
190
+ #
191
+
192
+ current, constants = const_name.to_s.constants_split
193
+ subclass = if constants.empty?
194
+ # The constant exists; validate the constant is a subclass of self.
195
+ unless current.kind_of?(Class) && current.ancestors.include?(self)
196
+ raise ArgumentError, "#{current} is already defined and is not a subclass of #{self}!"
197
+ end
198
+ current
199
+ else
200
+ # Generate the nesting module
201
+ subclass_const = constants.pop
202
+ constants.each {|const| current = current.const_set(const, Module.new)}
203
+
204
+ # Create and set the subclass constant
205
+ current.const_set(subclass_const, Class.new(self))
206
+ end
207
+
208
+ #
209
+ # Define the subclass
210
+ #
211
+
212
+ subclass.define_configurations(configs)
213
+ subclass.define_dependencies(dependencies)
214
+ subclass.define_process(block) if block_given?
215
+
216
+ #
217
+ # Register documentation
218
+ #
219
+
220
+ const_name = current == Object ? subclass_const : "#{current}::#{subclass_const}"
221
+ caller.each_with_index do |line, index|
222
+ case line
223
+ when /\/tap\/support\/declarations.rb/ then next
224
+ when Support::Lazydoc::CALLER_REGEXP
225
+ subclass.source_file = File.expand_path($1)
226
+ lzd = subclass.lazydoc(false)
227
+ lzd[const_name, false]['manifest'] = lzd.register($3.to_i - 1)
228
+ break
229
+ end
230
+ end
231
+
232
+ arity = options[:arity] || (block_given? ? block.arity : -1)
233
+ comment = Support::Comment.new
234
+ comment.subject = case
235
+ when arity > 0
236
+ Array.new(arity, "INPUT").join(' ')
237
+ when arity < 0
238
+ array = Array.new(-1 * arity - 1, "INPUT")
239
+ array << "INPUTS..."
240
+ array.join(' ')
241
+ else ""
242
+ end
243
+ subclass.lazydoc(false)[const_name, false]['args'] ||= comment
244
+
245
+ subclass.default_name = const_name.underscore
246
+ subclass
247
+ end
248
+
249
+ def instantiate(argv, app=Tap::App.instance) # => instance, argv
250
+ opts = OptionParser.new
251
+
252
+ # Add configurations
253
+ config = {}
254
+ unless configurations.empty?
255
+ opts.separator ""
256
+ opts.separator "configurations:"
257
+ end
258
+
259
+ configurations.each do |receiver, key, configuration|
260
+ opts.on(*Support::CommandLine.configv(configuration)) do |value|
261
+ config[key] = value
262
+ end
263
+ end
264
+
265
+ # Add options on_tail, giving priority to configurations
266
+ opts.separator ""
267
+ opts.separator "options:"
268
+
269
+ opts.on_tail("-h", "--help", "Print this help") do
270
+ opts.banner = "#{help}usage: tap run -- #{to_s.underscore} #{args.subject}"
271
+ puts opts
272
+ exit
273
+ end
274
+
275
+ # Add option for name
276
+ name = default_name
277
+ opts.on_tail('--name NAME', /^[^-].*/, 'Specify a name') do |value|
278
+ name = value
279
+ end
280
+
281
+ # Add option to add args
282
+ use_args = []
283
+ opts.on_tail('--use FILE', /^[^-].*/, 'Loads inputs from file') do |value|
284
+ obj = YAML.load_file(value)
285
+ case obj
286
+ when Hash
287
+ obj.values.each do |array|
288
+ # error if value isn't an array
289
+ use_args.concat(array)
290
+ end
291
+ when Array
292
+ use_args.concat(obj)
293
+ else
294
+ use_args << obj
295
+ end
296
+ end
297
+
298
+ opts.parse!(argv)
299
+ obj = new({}, name, app)
300
+
301
+ path_configs = load_config(app.config_filepath(name))
302
+ if path_configs.kind_of?(Array)
303
+ path_configs.each_with_index do |path_config, i|
304
+ obj.initialize_batch_obj(path_config, "#{name}_#{i}") unless i == 0
305
+ end
306
+ path_configs = path_configs[0]
307
+ end
308
+
309
+ argv = (argv + use_args).collect {|str| str =~ /\A---\s*\n/ ? YAML.load(str) : str }
310
+
311
+ [obj.reconfigure(path_configs).reconfigure(config), argv]
312
+ end
313
+
314
+ def lazydoc(resolve=true)
315
+ lazydoc = super(false)
316
+ lazydoc.register_method_pattern('args', :process) unless lazydoc.resolved?
317
+ super
318
+ end
319
+
320
+ DEFAULT_HELP_TEMPLATE = %Q{<% manifest = task_class.manifest %>
321
+ <%= task_class %><%= manifest.subject.to_s.strip.empty? ? '' : ' -- ' %><%= manifest.subject %>
322
+
323
+ <% unless manifest.empty? %>
324
+ <%= '-' * 80 %>
325
+
326
+ <% manifest.wrap(77, 2, nil).each do |line| %>
327
+ <%= line %>
328
+ <% end %>
329
+ <%= '-' * 80 %>
330
+ <% end %>
331
+
332
+ }
333
+ def help
334
+ Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
335
+ end
336
+
337
+ def depends_on(dependency_class, *args)
338
+ unless dependency_class.respond_to?(:instance)
339
+ raise ArgumentError, "dependency_class does not respond to instance: #{dependency_class}"
340
+ end
341
+ (dependencies << [dependency_class, args]).uniq!
342
+ self
343
+ end
344
+
345
+ protected
346
+
347
+ def dependency(name, dependency_class, *args)
348
+ depends_on(dependency_class, *args)
349
+
350
+ define_method(name) do
351
+ index = Support::Executable.index(dependency_class.instance, args)
352
+ Support::Executable.results[index]._current
353
+ end
354
+
355
+ public(name)
356
+ end
357
+
358
+ def define(name, klass=Tap::Task, &block)
359
+ instance_var = "@#{name}".to_sym
360
+
361
+ define_method(name) do |*args|
362
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" if args.length > 1
363
+
364
+ instance_name = args[0] || name
365
+ instance_variable_set(instance_var, {}) unless instance_variable_defined?(instance_var)
366
+ instance_variable_get(instance_var)[instance_name] ||= config_task(instance_name, klass, &block)
367
+ end
368
+
369
+ define_method("#{name}=") do |input|
370
+ input = {name => input} unless input.kind_of?(Hash)
371
+ instance_variable_set(instance_var, input)
372
+ end
373
+
374
+ public(name, "#{name}=")
375
+ end
376
+
377
+ def define_configurations(configs)
378
+ case configs
379
+ when Hash
380
+ # hash configs are simply added as default configurations
381
+ attr_accessor(*configs.keys)
382
+ configs.each_pair do |key, value|
383
+ configurations.add(key, value)
384
+ end
385
+ public(*configs.keys)
386
+ when Array
387
+ # array configs define configuration methods
388
+ configs.each do |method, key, value, opts, config_block|
389
+ send(method, key, value, opts, &config_block)
390
+ end
391
+ else
392
+ raise ArgumentError, "cannot define configurations from: #{configs}"
393
+ end
394
+ end
395
+
396
+ def define_dependencies(dependencies)
397
+ dependencies.each do |name, dependency_class, *args|
398
+ dependency(name, dependency_class, *args)
399
+ end if dependencies
400
+ end
401
+
402
+ def define_process(block)
403
+ send(:define_method, :process, &block)
404
+ end
405
+ end
406
+
407
+ instance_variable_set(:@source_file, __FILE__)
408
+ instance_variable_set(:@default_name, 'tap/task')
409
+ instance_variable_set(:@dependencies, [])
410
+ lazy_attr :manifest
411
+ lazy_attr :args
412
+
413
+ # The application used to load config_file templates
414
+ # (and hence, to initialize batched objects).
415
+ attr_reader :app
162
416
 
417
+ # The name of self.
418
+ #--
419
+ # Currently names may be any object. Audit makes use of name
420
+ # via to_s, as does app when figuring configuration filepaths.
421
+ attr_accessor :name
422
+
423
+ # The task block provided during initialization.
424
+ attr_reader :task_block
425
+
426
+ # Initializes a new instance and associated batch objects. Batch
427
+ # objects will be initialized for each configuration template
428
+ # specified by app.each_config_template(config_file) where
429
+ # config_file = app.config_filepath(name).
163
430
  def initialize(config={}, name=nil, app=App.instance, &task_block)
164
- super(config, name, app)
431
+ super()
165
432
 
433
+ @app = app
434
+ @name = name || self.class.default_name
166
435
  @task_block = (task_block == nil ? default_task_block : task_block)
436
+
437
+ @_method_name = :execute
167
438
  @multithread = false
168
439
  @on_complete_block = nil
169
- @_method_name = :execute
440
+ @dependencies = []
441
+
442
+ case config
443
+ when Support::InstanceConfiguration
444
+ @config = config
445
+ config.bind(self)
446
+ else
447
+ initialize_config(config)
448
+ end
449
+
450
+ self.class.dependencies.each do |task_class, args|
451
+ depends_on(task_class.instance, *args)
452
+ end
453
+ end
454
+
455
+ # Creates a new batched object and adds the object to batch. The batched object
456
+ # will be a duplicate of the current object but with a new name and/or
457
+ # configurations.
458
+ def initialize_batch_obj(overrides={}, name=nil)
459
+ obj = super().reconfigure(overrides)
460
+ obj.name = name if name
461
+ obj
170
462
  end
171
463
 
172
464
  # Enqueues self and self.batch to app with the inputs.
@@ -175,7 +467,7 @@ module Tap
175
467
  def enq(*inputs)
176
468
  app.queue.enq(self, inputs)
177
469
  end
178
-
470
+
179
471
  batch_function :enq, :multithread=
180
472
  batch_function(:on_complete) {}
181
473
 
@@ -237,6 +529,26 @@ module Tap
237
529
  task_block.call(*inputs)
238
530
  end
239
531
 
532
+ # Logs the inputs to the application logger (via app.log)
533
+ def log(action, msg="", level=Logger::INFO)
534
+ # TODO - add a task identifier?
535
+ app.log(action, msg, level)
536
+ end
537
+
538
+ # Raises a TerminateError if app.state == State::TERMINATE.
539
+ # check_terminate may be called at any time to provide a
540
+ # breakpoint in long-running processes.
541
+ def check_terminate
542
+ if app.state == App::State::TERMINATE
543
+ raise App::TerminateError.new
544
+ end
545
+ end
546
+
547
+ # Returns self.name
548
+ def to_s
549
+ name.to_s
550
+ end
551
+
240
552
  protected
241
553
 
242
554
  # Hook to set a default task block. By default, nil.
@@ -255,5 +567,13 @@ module Tap
255
567
  def on_execute_error(err)
256
568
  raise err
257
569
  end
570
+
571
+ private
572
+
573
+ def config_task(name, klass=Tap::Task, &block)
574
+ configs = config[name] || {}
575
+ raise ArgumentError, "config '#{name}' is not a hash" unless configs.kind_of?(Hash)
576
+ klass.new(configs, name, &block)
577
+ end
258
578
  end
259
579
  end