scout-gear 2.0.0 → 5.1.1

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +59 -2
  3. data/VERSION +1 -1
  4. data/bin/scout +231 -24
  5. data/lib/scout/cmd.rb +344 -0
  6. data/lib/scout/concurrent_stream.rb +259 -0
  7. data/lib/scout/exceptions.rb +15 -8
  8. data/lib/scout/indiferent_hash/options.rb +8 -26
  9. data/lib/scout/log/color.rb +2 -2
  10. data/lib/scout/log/fingerprint.rb +11 -1
  11. data/lib/scout/log/progress/report.rb +0 -1
  12. data/lib/scout/log/progress/util.rb +1 -1
  13. data/lib/scout/log/progress.rb +4 -4
  14. data/lib/scout/log.rb +10 -2
  15. data/lib/scout/meta_extension.rb +15 -1
  16. data/lib/scout/misc/digest.rb +56 -0
  17. data/lib/scout/misc/filesystem.rb +26 -0
  18. data/lib/scout/misc/format.rb +1 -2
  19. data/lib/scout/misc/insist.rb +56 -0
  20. data/lib/scout/misc.rb +4 -11
  21. data/lib/scout/open/lock.rb +61 -0
  22. data/lib/scout/open/remote.rb +120 -0
  23. data/lib/scout/open/stream.rb +372 -0
  24. data/lib/scout/open/util.rb +225 -0
  25. data/lib/scout/open.rb +169 -0
  26. data/lib/scout/path/find.rb +67 -21
  27. data/lib/scout/path/tmpfile.rb +8 -0
  28. data/lib/scout/path/util.rb +14 -1
  29. data/lib/scout/path.rb +6 -30
  30. data/lib/scout/persist/open.rb +17 -0
  31. data/lib/scout/persist/path.rb +15 -0
  32. data/lib/scout/persist/serialize.rb +140 -0
  33. data/lib/scout/persist.rb +54 -0
  34. data/lib/scout/resource/path.rb +15 -0
  35. data/lib/scout/resource/produce/rake.rb +69 -0
  36. data/lib/scout/resource/produce.rb +246 -0
  37. data/lib/scout/resource/scout.rb +3 -0
  38. data/lib/scout/resource.rb +37 -0
  39. data/lib/scout/simple_opt/accessor.rb +1 -1
  40. data/lib/scout/simple_opt/doc.rb +4 -22
  41. data/lib/scout/simple_opt/parse.rb +4 -3
  42. data/lib/scout/tmpfile.rb +39 -1
  43. data/lib/scout/workflow/definition.rb +72 -0
  44. data/lib/scout/workflow/documentation.rb +77 -0
  45. data/lib/scout/workflow/step/info.rb +77 -0
  46. data/lib/scout/workflow/step.rb +96 -0
  47. data/lib/scout/workflow/task/inputs.rb +112 -0
  48. data/lib/scout/workflow/task.rb +141 -0
  49. data/lib/scout/workflow/usage.rb +294 -0
  50. data/lib/scout/workflow/util.rb +11 -0
  51. data/lib/scout/workflow.rb +39 -0
  52. data/lib/scout-gear.rb +4 -0
  53. data/lib/scout.rb +1 -0
  54. data/lib/workflow-scout.rb +2 -0
  55. data/scout-gear.gemspec +66 -5
  56. data/scout_commands/alias +48 -0
  57. data/scout_commands/find +83 -0
  58. data/scout_commands/glob +0 -0
  59. data/scout_commands/rbbt +23 -0
  60. data/scout_commands/workflow/task +707 -0
  61. data/test/scout/indiferent_hash/test_options.rb +11 -1
  62. data/test/scout/misc/test_digest.rb +30 -0
  63. data/test/scout/misc/test_filesystem.rb +30 -0
  64. data/test/scout/misc/test_insist.rb +13 -0
  65. data/test/scout/open/test_lock.rb +52 -0
  66. data/test/scout/open/test_remote.rb +25 -0
  67. data/test/scout/open/test_stream.rb +515 -0
  68. data/test/scout/open/test_util.rb +73 -0
  69. data/test/scout/path/test_find.rb +28 -0
  70. data/test/scout/persist/test_open.rb +37 -0
  71. data/test/scout/persist/test_path.rb +37 -0
  72. data/test/scout/persist/test_serialize.rb +114 -0
  73. data/test/scout/resource/test_path.rb +40 -0
  74. data/test/scout/resource/test_produce.rb +62 -0
  75. data/test/scout/test_cmd.rb +85 -0
  76. data/test/scout/test_concurrent_stream.rb +29 -0
  77. data/test/scout/test_misc.rb +0 -7
  78. data/test/scout/test_open.rb +146 -0
  79. data/test/scout/test_path.rb +3 -1
  80. data/test/scout/test_persist.rb +83 -0
  81. data/test/scout/test_resource.rb +26 -0
  82. data/test/scout/test_workflow.rb +87 -0
  83. data/test/scout/workflow/step/test_info.rb +28 -0
  84. data/test/scout/workflow/task/test_inputs.rb +182 -0
  85. data/test/scout/workflow/test_step.rb +36 -0
  86. data/test/scout/workflow/test_task.rb +178 -0
  87. data/test/scout/workflow/test_usage.rb +26 -0
  88. data/test/scout/workflow/test_util.rb +17 -0
  89. data/test/test_helper.rb +17 -0
  90. data/test/test_scout-gear.rb +0 -0
  91. metadata +64 -3
@@ -0,0 +1,69 @@
1
+ require_relative '../../misc'
2
+ require_relative '../../path'
3
+ require 'rake'
4
+
5
+ class Rake::FileTask
6
+ class << self
7
+ alias_method :old_define_task, :define_task
8
+ end
9
+
10
+ def self.define_task(file, *args, &block)
11
+ @@files ||= []
12
+ @@files << file
13
+ old_define_task(file, *args, &block)
14
+ end
15
+
16
+ def self.files
17
+ @@files
18
+ end
19
+
20
+ def self.clear_files
21
+ @@files = []
22
+ end
23
+ end
24
+
25
+ module ScoutRake
26
+ class TaskNotFound < StandardError; end
27
+ def self.run(rakefile, dir, task, &block)
28
+ old_pwd = FileUtils.pwd
29
+
30
+ Rake::Task.clear
31
+ Rake::FileTask.clear_files
32
+
33
+ t = nil
34
+ pid = Process.fork{
35
+ if block_given?
36
+ TOPLEVEL_BINDING.receiver.instance_exec &block
37
+ else
38
+ if Path.is_filename? rakefile
39
+ rakefile = rakefile.produce.find
40
+ load rakefile
41
+ else
42
+ TmpFile.with_file(rakefile) do |tmpfile|
43
+ load tmpfile
44
+ end
45
+ end
46
+ end
47
+
48
+ raise TaskNotFound if Rake::Task[task].nil?
49
+
50
+ #Misc.pre_fork
51
+ begin
52
+ Misc.in_dir(dir) do
53
+ Rake::Task[task].invoke
54
+
55
+ Rake::Task.clear
56
+ Rake::FileTask.clear_files
57
+ end
58
+ rescue Exception
59
+ Log.error "Error in rake: #{$!.message}"
60
+ Log.exception $!
61
+ Kernel.exit! -1
62
+ end
63
+ Kernel.exit! 0
64
+ }
65
+ Process.waitpid(pid)
66
+ raise "Rake failed" unless $?.success?
67
+
68
+ end
69
+ end
@@ -0,0 +1,246 @@
1
+ require_relative '../open'
2
+ require_relative '../tmpfile'
3
+ require_relative 'produce/rake'
4
+
5
+ module Resource
6
+ def claim(path, type, content = nil, &block)
7
+ if type == :rake
8
+ @rake_dirs ||= {}
9
+ @rake_dirs[path] = content || block
10
+ else
11
+ @resources ||= {}
12
+ @resources[path] = [type, content || block]
13
+ end
14
+ end
15
+
16
+ def rake_for(path)
17
+ @rake_dirs ||= {}
18
+ @rake_dirs.select{|dir, content|
19
+ Misc.path_relative_to(dir, path)
20
+ }.sort_by{|dir, content|
21
+ dir.length
22
+ }.last
23
+ end
24
+
25
+ def has_rake(path)
26
+ !! rake_for(path)
27
+ end
28
+
29
+ def run_rake(path, rakefile, rake_dir)
30
+ task = Misc.path_relative_to rake_dir, path
31
+ rakefile = rakefile.produce if rakefile.respond_to? :produce
32
+ rakefile = rakefile.find if rakefile.respond_to? :find
33
+
34
+ rake_dir = rake_dir.find(:user) if rake_dir.respond_to? :find
35
+
36
+ begin
37
+ if Proc === rakefile
38
+ ScoutRake.run(nil, rake_dir, task, &rakefile)
39
+ else
40
+ ScoutRake.run(rakefile, rake_dir, task)
41
+ end
42
+ rescue Rake::TaskNotFound
43
+ if rake_dir.nil? or rake_dir.empty? or rake_dir == "/" or rake_dir == "./"
44
+ raise $!
45
+ end
46
+ task = File.join(File.basename(rake_dir), task)
47
+ rake_dir = File.dirname(rake_dir)
48
+ retry
49
+ end
50
+ end
51
+
52
+ def produce(path, force = false)
53
+ case
54
+ when @resources.include?(path)
55
+ type, content = @resources[path]
56
+ when (Path === path && @resources.include?(path.original))
57
+ type, content = @resources[path.original]
58
+ when has_rake(path)
59
+ type = :rake
60
+ rake_dir, content = rake_for(path)
61
+ rake_dir = Path.setup(rake_dir.dup, self.pkgdir, self)
62
+ else
63
+ if path !~ /\.(gz|bgz)$/
64
+ begin
65
+ produce(path.annotate(path + '.gz'), force)
66
+ rescue ResourceNotFound
67
+ begin
68
+ produce(path.annotate(path + '.bgz'), force)
69
+ rescue ResourceNotFound
70
+ raise ResourceNotFound, "Resource is missing and does not seem to be claimed: #{ self } -- #{ path } "
71
+ end
72
+ end
73
+ else
74
+ raise ResourceNotFound, "Resource is missing and does not seem to be claimed: #{ self } -- #{ path } "
75
+ end
76
+ end
77
+
78
+ if path.respond_to?(:find)
79
+ final_path = force ? path.find(:default) : path.find
80
+ else
81
+ final_path = path
82
+ end
83
+
84
+ if type and not File.exist?(final_path) or force
85
+ Log.medium "Producing: (#{self.to_s}) #{ final_path }"
86
+ lock_filename = TmpFile.tmp_for_file(final_path, :dir => lock_dir)
87
+
88
+ Open.lock lock_filename do
89
+ FileUtils.rm_rf final_path if force and File.exist? final_path
90
+
91
+ if ! File.exist?(final_path) || force
92
+
93
+ begin
94
+ case type
95
+ when :string
96
+ Open.sensible_write(final_path, content)
97
+ when :csv
98
+ raise "TSV/CSV Not implemented yet"
99
+ #require 'rbbt/tsv/csv'
100
+ #tsv = TSV.csv Open.open(content)
101
+ #Open.sensible_write(final_path, tsv.to_s)
102
+ when :url
103
+ options = {}
104
+ options[:noz] = true if Open.gzip?(final_path) || Open.bgzip?(final_path) || Open.zip?(final_path)
105
+ Open.sensible_write(final_path, Open.open(content, options))
106
+ when :proc
107
+ data = case content.arity
108
+ when 0
109
+ content.call
110
+ when 1
111
+ content.call final_path
112
+ end
113
+ case data
114
+ when String, IO, StringIO
115
+ Open.sensible_write(final_path, data)
116
+ when Array
117
+ Open.sensible_write(final_path, data * "\n")
118
+ when TSV
119
+ Open.sensible_write(final_path, data.dumper_stream)
120
+ when TSV::Dumper
121
+ Open.sensible_write(final_path, data.stream)
122
+ when nil
123
+ else
124
+ raise "Unkown object produced: #{Log.fingerprint data}"
125
+ end
126
+ when :rake
127
+ run_rake(path, content, rake_dir)
128
+ when :install
129
+ Log.debug "Installing software: #{path}"
130
+
131
+ $set_software_env = false unless File.exist? path
132
+
133
+ software_dir = path.resource.root.software.find :user
134
+ helper_file = File.expand_path(Rbbt.share.install.software.lib.install_helpers.find(:lib, caller_lib_dir(__FILE__)))
135
+ #helper_file = File.expand_path(Rbbt.share.install.software.lib.install_helpers.find)
136
+
137
+ preamble = <<-EOF
138
+ #!/bin/bash
139
+
140
+ RBBT_SOFTWARE_DIR="#{software_dir}"
141
+
142
+ INSTALL_HELPER_FILE="#{helper_file}"
143
+ source "$INSTALL_HELPER_FILE"
144
+ EOF
145
+
146
+ content = content.call if Proc === content
147
+
148
+ content = if content =~ /git:|\.git$/
149
+ {:git => content}
150
+ else
151
+ {:src => content}
152
+ end if String === content and Open.remote?(content)
153
+
154
+ script_text = case content
155
+ when nil
156
+ raise "No way to install #{path}"
157
+ when Path
158
+ Open.read(content)
159
+ when String
160
+ if Path.is_filename?(content) and Open.exists?(content)
161
+ Open.read(content)
162
+ else
163
+ content
164
+ end
165
+ when Hash
166
+ name = content[:name] || File.basename(path)
167
+ git = content[:git]
168
+ src = content[:src]
169
+ url = content[:url]
170
+ jar = content[:jar]
171
+ extra = content[:extra]
172
+ commands = content[:commands]
173
+ if git
174
+ <<-EOF
175
+
176
+ name='#{name}'
177
+ url='#{git}'
178
+
179
+ install_git "$name" "$url" #{extra}
180
+
181
+ #{commands}
182
+ EOF
183
+ elsif src
184
+ <<-EOF
185
+
186
+ name='#{name}'
187
+ url='#{src}'
188
+
189
+ install_src "$name" "$url" #{extra}
190
+
191
+ #{commands}
192
+ EOF
193
+ elsif jar
194
+ <<-EOF
195
+
196
+ name='#{name}'
197
+ url='#{jar}'
198
+
199
+ install_jar "$name" "$url" #{extra}
200
+
201
+ #{commands}
202
+ EOF
203
+ else
204
+ <<-EOF
205
+
206
+ name='#{name}'
207
+ url='#{url}'
208
+
209
+ #{commands}
210
+ EOF
211
+ end
212
+ end
213
+
214
+ script = preamble + "\n" + script_text
215
+ Log.debug "Installing software with script:\n" << script
216
+ CMD.cmd_log('bash', :in => script)
217
+
218
+ set_software_env(software_dir) unless $set_software_env
219
+ $set_software_env = true
220
+ else
221
+ raise "Could not produce #{ resource }. (#{ type }, #{ content })"
222
+ end
223
+ rescue
224
+ FileUtils.rm_rf final_path if File.exist? final_path
225
+ raise $!
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ # After producing a file, make sure we recheck all locations, the file
232
+ # might have appeared with '.gz' extension for instance
233
+ path.instance_variable_set("@path", {})
234
+
235
+ path
236
+ end
237
+
238
+ end
239
+
240
+ module Path
241
+ def produce(force = false)
242
+ return self if ! force && Open.exist?(self)
243
+ self.pkgdir.produce self if Resource === self.pkgdir
244
+ return self
245
+ end
246
+ end
@@ -0,0 +1,3 @@
1
+ module Scout
2
+ extend Resource
3
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'log'
2
+ require_relative 'path'
3
+ require_relative 'resource/produce'
4
+
5
+ module Resource
6
+ extend MetaExtension
7
+ extension_attr :pkgdir, :libdir, :subdir, :resources, :rake_dirs, :path_maps, :lock_dir
8
+
9
+ def self.default_lock_dir
10
+ Path.setup('tmp/produce_locks').find
11
+ end
12
+
13
+ def subdir
14
+ @subdir ||= ""
15
+ end
16
+
17
+ def lock_dir
18
+ @lock_dir ||= Resource.default_lock_dir
19
+ end
20
+
21
+ def pkgdir
22
+ @pkgdir ||= Path.default_pkgdir
23
+ end
24
+
25
+ def root
26
+ Path.setup(subdir, self, self.libdir, @path_maps)
27
+ end
28
+
29
+ def method_missing(name, prev = nil, *args)
30
+ if prev.nil?
31
+ root.send(name, *args)
32
+ else
33
+ root.send(name, prev, *args)
34
+ end
35
+ end
36
+ end
37
+
@@ -1,6 +1,6 @@
1
1
  module SOPT
2
2
  class << self
3
- attr_accessor :inputs, :input_shortcuts, :input_types, :input_descriptions, :input_defaults
3
+ attr_writer :inputs, :input_shortcuts, :input_types, :input_descriptions, :input_defaults
4
4
  end
5
5
 
6
6
  def self.all
@@ -2,7 +2,7 @@ require_relative '../log'
2
2
  module SOPT
3
3
 
4
4
  class << self
5
- attr_accessor :command, :summary, :synopsys, :description
5
+ attr_writer :command, :summary, :synopsys, :description
6
6
  end
7
7
 
8
8
  def self.command
@@ -73,32 +73,12 @@ module SOPT
73
73
  register(shortcut, name, type, description) unless self.inputs.include? name
74
74
 
75
75
  name = SOPT.input_format(name, type.to_sym, default, shortcut)
76
- description
77
76
  Misc.format_definition_list_item(name, description, 80, 31, nil)
78
77
  end * "\n"
79
78
  end
80
79
 
81
80
  def self.doc
82
- doc = <<-EOF
83
- #{Log.color :magenta}#{command}(1) -- #{summary}
84
- #{"=" * (command.length + summary.length + 7)}#{Log.color :reset}
85
-
86
- #{ Log.color :magenta, "## SYNOPSYS"}
87
-
88
- #{Log.color :blue, synopsys}
89
-
90
- #{ Log.color :magenta, "## DESCRIPTION"}
91
-
92
- #{Misc.format_paragraph description}
93
-
94
- #{ Log.color :magenta, "## OPTIONS"}
95
-
96
- #{input_doc(inputs, input_types, input_descriptions, input_defaults, input_shortcuts)}
97
- EOF
98
- end
99
-
100
- def self.doc
101
- doc = <<-EOF
81
+ doc =<<-EOF
102
82
  #{Log.color :magenta}#{command}(1) -- #{summary}
103
83
  #{"=" * (command.length + summary.length + 7)}#{Log.color :reset}
104
84
 
@@ -116,5 +96,7 @@ module SOPT
116
96
 
117
97
  doc << Log.color(:magenta, "## OPTIONS") << "\n\n"
118
98
  doc << input_doc(inputs, input_types, input_descriptions, input_defaults, input_shortcuts)
99
+
100
+ doc
119
101
  end
120
102
  end
@@ -44,23 +44,24 @@ module SOPT
44
44
  end
45
45
 
46
46
  def self.parse(opt_str)
47
- info = {}
48
-
49
47
  inputs = []
48
+
50
49
  if opt_str.include? "\n"
51
50
  re = /\n+/
52
51
  else
53
52
  re = /:/
54
53
  end
54
+
55
55
  opt_str.split(re).each do |entry|
56
56
  entry.strip!
57
57
  next if entry.empty?
58
- names, _sep, description = entry.partition /\s+/
58
+ names, _sep, description = entry.partition(/\s+/)
59
59
  short, long, asterisk = names.match(/\s*(?:-(.+))?(?:--(.+?))([*])?$/).values_at 1,2,3
60
60
 
61
61
  inputs << long
62
62
  register short, long, asterisk, description
63
63
  end
64
+
64
65
  inputs
65
66
  end
66
67
  end
data/lib/scout/tmpfile.rb CHANGED
@@ -1,7 +1,10 @@
1
- require 'fileutils'
2
1
  require_relative 'misc'
2
+ require_relative 'log'
3
+ require 'fileutils'
3
4
 
4
5
  module TmpFile
6
+ MAX_FILE_LENGTH = 150
7
+
5
8
  def self.user_tmp(subdir = nil)
6
9
  if subdir
7
10
  File.join(ENV["HOME"],"/tmp/scout", subdir)
@@ -89,4 +92,39 @@ module TmpFile
89
92
  end
90
93
  end
91
94
  end
95
+
96
+ def self.tmp_for_file(file, tmp_options = {}, other_options = {})
97
+ tmp_for_file = IndiferentHash.process_options tmp_options, :file
98
+ return tmp_for_file unless tmp_for_file.nil?
99
+
100
+ prefix = IndiferentHash.process_options tmp_options, :prefix
101
+
102
+ if prefix.nil?
103
+ perfile = file.to_s.gsub(/\//, '>')
104
+ else
105
+ perfile = prefix.to_s + ":" + file.to_s.gsub(/\//, '>')
106
+ end
107
+
108
+ perfile.sub!(/\.b?gz$/,'')
109
+
110
+ if other_options.include? :filters
111
+ other_options[:filters].each do |match,value|
112
+ perfile = perfile + "&F[#{match}=#{Misc.digest(value)}]"
113
+ end
114
+ end
115
+
116
+ persistence_dir = IndiferentHash.process_options(tmp_options, :dir) || TmpFile.tmpdir
117
+ Path.setup(persistence_dir) unless Path === persistence_dir
118
+
119
+ filename = perfile.gsub(/\s/,'_').gsub(/\//,'>')
120
+ clean_options = other_options.dup
121
+ clean_options.delete :unnamed
122
+ clean_options.delete "unnamed"
123
+
124
+ filename = filename[0..MAX_FILE_LENGTH] << Misc.digest(filename[MAX_FILE_LENGTH+1..-1]) if filename.length > MAX_FILE_LENGTH + 10
125
+
126
+ filename += ":" << Misc.digest(clean_options) unless clean_options.empty?
127
+
128
+ persistence_dir[filename]
129
+ end
92
130
  end
@@ -0,0 +1,72 @@
1
+ require_relative '../meta_extension'
2
+
3
+ module Workflow
4
+ extend MetaExtension
5
+ extension_attr :name, :tasks
6
+
7
+ class << self
8
+ attr_accessor :directory
9
+
10
+ def directory
11
+ @directory ||= Path.setup('var/jobs')
12
+ end
13
+
14
+ end
15
+
16
+ def name
17
+ @name ||= self.to_s
18
+ end
19
+
20
+ attr_accessor :directory
21
+ def directory
22
+ @directory ||= Workflow.directory[name]
23
+ end
24
+
25
+ def directory=(directory)
26
+ @directory = directory
27
+ @tasks.each{|name,d| d.directory = directory[name] } if @tasks
28
+ end
29
+
30
+ def annotate_next_task(type, obj)
31
+ @annotate_next_task ||= {}
32
+ @annotate_next_task[type] ||= []
33
+ @annotate_next_task[type] << obj
34
+ end
35
+
36
+ def dep(*args, &block)
37
+ case args.length
38
+ when 3
39
+ workflow, task, options = args
40
+ when 2
41
+ if Hash === args.last
42
+ task, options = args
43
+ else
44
+ workflow, task = args
45
+ end
46
+ when 1
47
+ task = args.first
48
+ end
49
+ workflow = self if workflow.nil?
50
+ options = {} if options.nil?
51
+ annotate_next_task :deps, [workflow, task, options, block, args]
52
+ end
53
+
54
+ def input(*args)
55
+ annotate_next_task(:inputs, args)
56
+ end
57
+
58
+ def task(name_and_type, &block)
59
+ name, type = name_and_type.collect.first
60
+ @tasks ||= IndiferentHash.setup({})
61
+ begin
62
+ @annotate_next_task ||= {}
63
+ @tasks[name] = Task.setup(block, @annotate_next_task.merge(name: name, type: type, directory: directory[name]))
64
+ ensure
65
+ @annotate_next_task = {}
66
+ end
67
+ end
68
+
69
+ def desc(description)
70
+ annotate_next_task(:desc, description)
71
+ end
72
+ end
@@ -0,0 +1,77 @@
1
+ module Workflow
2
+
3
+ def self.doc_parse_first_line(str)
4
+ if str.match(/^([^\n]*)\n\n(.*)/sm)
5
+ str.replace $2
6
+ $1
7
+ else
8
+ ""
9
+ end
10
+ end
11
+
12
+ def self.doc_parse_up_to(str, pattern, keep = false)
13
+ pre, _pat, _post = str.partition pattern
14
+ if _pat
15
+ [pre, (keep ? _pat << _post : _post)]
16
+ else
17
+ _post
18
+ end
19
+ end
20
+
21
+ def self.doc_parse_chunks(str, pattern)
22
+ parts = str.split(pattern)
23
+ return {} if parts.length < 2
24
+ tasks = Hash[*parts[1..-1].collect{|v| v.strip}]
25
+ tasks.delete_if{|t,d| d.empty?}
26
+ tasks
27
+ end
28
+
29
+ def self.parse_workflow_doc(doc)
30
+ title = doc_parse_first_line doc
31
+ description, task_info = doc_parse_up_to doc, /^# Tasks/i
32
+ task_description, tasks = doc_parse_up_to task_info, /^##/, true
33
+ tasks = doc_parse_chunks tasks, /## (.*)/
34
+ {:title => title.strip, :description => description.strip, :task_description => task_description.strip, :tasks => tasks}
35
+ end
36
+
37
+ def documentation_markdown
38
+ return "" if @libdir.nil?
39
+ file = @libdir['workflow.md'].find
40
+ file = @libdir['README.md'].find unless file.exists?
41
+ if file.exists?
42
+ file.read
43
+ else
44
+ ""
45
+ end
46
+ end
47
+
48
+ def load_documentation
49
+ return if @documentation
50
+ @documentation ||= Workflow.parse_workflow_doc documentation_markdown
51
+ @documentation[:tasks].each do |task, description|
52
+ if task.include? "#"
53
+ workflow, task = task.split("#")
54
+ workflow = begin
55
+ Kernel.const_get workflow
56
+ rescue
57
+ next
58
+ end
59
+ else
60
+ workflow = self
61
+ end
62
+
63
+ task = task.to_sym
64
+ if workflow.tasks.include? task
65
+ workflow.tasks[task].description = description
66
+ else
67
+ Log.low "Documentation for #{ task }, but not a #{ workflow.to_s } task"
68
+ end
69
+ end
70
+ end
71
+
72
+ attr_accessor :documentation
73
+ def documentation
74
+ load_documentation if @documentation.nil?
75
+ @documentation
76
+ end
77
+ end