tap 0.12.4 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/History +34 -0
  2. data/README +62 -41
  3. data/bin/tap +36 -40
  4. data/cmd/console.rb +14 -6
  5. data/cmd/manifest.rb +62 -58
  6. data/cmd/run.rb +49 -31
  7. data/doc/API +84 -0
  8. data/doc/Class Reference +83 -115
  9. data/doc/Examples/Command Line +36 -0
  10. data/doc/Examples/Workflow +40 -0
  11. data/lib/tap/app.rb +293 -214
  12. data/lib/tap/app/node.rb +43 -0
  13. data/lib/tap/app/queue.rb +77 -0
  14. data/lib/tap/app/stack.rb +16 -0
  15. data/lib/tap/app/state.rb +22 -0
  16. data/lib/tap/constants.rb +2 -2
  17. data/lib/tap/env.rb +400 -314
  18. data/lib/tap/env/constant.rb +227 -0
  19. data/lib/tap/env/gems.rb +63 -0
  20. data/lib/tap/env/manifest.rb +89 -0
  21. data/lib/tap/env/minimap.rb +292 -0
  22. data/lib/tap/{support → env}/string_ext.rb +2 -2
  23. data/lib/tap/exe.rb +113 -125
  24. data/lib/tap/join.rb +175 -0
  25. data/lib/tap/joins.rb +9 -0
  26. data/lib/tap/joins/switch.rb +44 -0
  27. data/lib/tap/joins/sync.rb +99 -0
  28. data/lib/tap/root.rb +100 -491
  29. data/lib/tap/root/utils.rb +220 -0
  30. data/lib/tap/{support → root}/versions.rb +31 -29
  31. data/lib/tap/schema.rb +248 -0
  32. data/lib/tap/schema/parser.rb +413 -0
  33. data/lib/tap/schema/utils.rb +82 -0
  34. data/lib/tap/support/intern.rb +19 -6
  35. data/lib/tap/support/templater.rb +8 -3
  36. data/lib/tap/task.rb +175 -171
  37. data/lib/tap/tasks/dump.rb +58 -0
  38. data/lib/tap/tasks/load.rb +62 -0
  39. metadata +30 -73
  40. data/cmd/destroy.rb +0 -27
  41. data/cmd/generate.rb +0 -27
  42. data/doc/Command Reference +0 -105
  43. data/doc/Syntax Reference +0 -234
  44. data/doc/Tutorial +0 -348
  45. data/lib/tap/dump.rb +0 -142
  46. data/lib/tap/file_task.rb +0 -384
  47. data/lib/tap/generator/arguments.rb +0 -13
  48. data/lib/tap/generator/base.rb +0 -176
  49. data/lib/tap/generator/destroy.rb +0 -60
  50. data/lib/tap/generator/generate.rb +0 -93
  51. data/lib/tap/generator/generators/command/command_generator.rb +0 -21
  52. data/lib/tap/generator/generators/command/templates/command.erb +0 -32
  53. data/lib/tap/generator/generators/config/config_generator.rb +0 -98
  54. data/lib/tap/generator/generators/generator/generator_generator.rb +0 -37
  55. data/lib/tap/generator/generators/generator/templates/task.erb +0 -27
  56. data/lib/tap/generator/generators/generator/templates/test.erb +0 -26
  57. data/lib/tap/generator/generators/root/root_generator.rb +0 -84
  58. data/lib/tap/generator/generators/root/templates/MIT-LICENSE +0 -22
  59. data/lib/tap/generator/generators/root/templates/README +0 -14
  60. data/lib/tap/generator/generators/root/templates/Rakefile +0 -84
  61. data/lib/tap/generator/generators/root/templates/Rapfile +0 -11
  62. data/lib/tap/generator/generators/root/templates/gemspec +0 -27
  63. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -3
  64. data/lib/tap/generator/generators/task/task_generator.rb +0 -25
  65. data/lib/tap/generator/generators/task/templates/task.erb +0 -14
  66. data/lib/tap/generator/generators/task/templates/test.erb +0 -19
  67. data/lib/tap/generator/manifest.rb +0 -20
  68. data/lib/tap/generator/preview.rb +0 -69
  69. data/lib/tap/load.rb +0 -64
  70. data/lib/tap/spec.rb +0 -41
  71. data/lib/tap/support/aggregator.rb +0 -65
  72. data/lib/tap/support/audit.rb +0 -333
  73. data/lib/tap/support/constant.rb +0 -143
  74. data/lib/tap/support/constant_manifest.rb +0 -126
  75. data/lib/tap/support/dependencies.rb +0 -54
  76. data/lib/tap/support/dependency.rb +0 -44
  77. data/lib/tap/support/executable.rb +0 -198
  78. data/lib/tap/support/executable_queue.rb +0 -125
  79. data/lib/tap/support/gems.rb +0 -43
  80. data/lib/tap/support/join.rb +0 -144
  81. data/lib/tap/support/joins.rb +0 -12
  82. data/lib/tap/support/joins/switch.rb +0 -27
  83. data/lib/tap/support/joins/sync_merge.rb +0 -38
  84. data/lib/tap/support/manifest.rb +0 -171
  85. data/lib/tap/support/minimap.rb +0 -90
  86. data/lib/tap/support/node.rb +0 -176
  87. data/lib/tap/support/parser.rb +0 -450
  88. data/lib/tap/support/schema.rb +0 -385
  89. data/lib/tap/support/shell_utils.rb +0 -67
  90. data/lib/tap/test.rb +0 -77
  91. data/lib/tap/test/assertions.rb +0 -38
  92. data/lib/tap/test/env_vars.rb +0 -29
  93. data/lib/tap/test/extensions.rb +0 -73
  94. data/lib/tap/test/file_test.rb +0 -362
  95. data/lib/tap/test/file_test_class.rb +0 -15
  96. data/lib/tap/test/regexp_escape.rb +0 -87
  97. data/lib/tap/test/script_test.rb +0 -46
  98. data/lib/tap/test/script_tester.rb +0 -115
  99. data/lib/tap/test/subset_test.rb +0 -260
  100. data/lib/tap/test/subset_test_class.rb +0 -99
  101. data/lib/tap/test/tap_test.rb +0 -109
  102. data/lib/tap/test/utils.rb +0 -231
@@ -0,0 +1,220 @@
1
+ require 'tap/root/versions'
2
+ autoload(:FileUtils, 'fileutils')
3
+
4
+ module Tap
5
+ class Root
6
+
7
+ # A variety of utility methods for working with paths.
8
+ module Utils
9
+ include Versions
10
+
11
+ # Regexp to match a windows-style root path.
12
+ WIN_ROOT_PATTERN = /^[A-z]:\//
13
+
14
+ module_function
15
+
16
+ # Returns the path of 'path' relative to dir. Both dir and path will
17
+ # be expanded to dir_string, if specified, before the relative path is
18
+ # determined. Returns nil if the path is not relative to dir.
19
+ #
20
+ # relative_path('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
21
+ #
22
+ def relative_path(dir, path, dir_string=Dir.pwd)
23
+ if dir_string
24
+ dir = File.expand_path(dir, dir_string)
25
+ path = File.expand_path(path, dir_string)
26
+ end
27
+ return nil unless Utils.relative?(dir, path, false)
28
+
29
+ # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
30
+ # as in: relative_path('/path', '/path') then the first arg returns nil, and an
31
+ # empty string is returned
32
+ path[(dir.chomp("/").length + 1)..-1] || ""
33
+ end
34
+
35
+ # Generates a target path translated from the source_dir to the
36
+ # target_dir. Raises an error if the path is not relative to the
37
+ # source_dir.
38
+ #
39
+ # translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
40
+ #
41
+ def translate(path, source_dir, target_dir)
42
+ unless relative_path = relative_path(source_dir, path)
43
+ raise ArgumentError, "\n#{path}\nis not relative to:\n#{source_dir}"
44
+ end
45
+ File.join(target_dir, relative_path)
46
+ end
47
+
48
+ # Returns the path, exchanging the extension with extname. Extname may
49
+ # optionally omit the leading period.
50
+ #
51
+ # exchange('path/to/file.txt', '.html') # => 'path/to/file.html'
52
+ # exchange('path/to/file.txt', 'rb') # => 'path/to/file.rb'
53
+ #
54
+ def exchange(path, extname)
55
+ "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
56
+ end
57
+
58
+ # Lists all unique paths matching the input glob patterns.
59
+ def glob(*patterns)
60
+ Dir[*patterns].uniq
61
+ end
62
+
63
+ # Lists all unique versions of path matching the glob version patterns. If
64
+ # no patterns are specified, then all versions of path will be returned.
65
+ def version_glob(path, *vpatterns)
66
+ paths = []
67
+
68
+ vpatterns << "*" if vpatterns.empty?
69
+ vpatterns.each do |vpattern|
70
+ paths.concat Dir.glob(version(path, vpattern))
71
+
72
+ # extra work to include the default version path for any version
73
+ paths << path if vpattern == "*" && File.exists?(path)
74
+ end
75
+
76
+ paths.uniq
77
+ end
78
+
79
+ # Path suffix glob. Globs along the paths for the specified suffix
80
+ # pattern.
81
+ def suffix_glob(suffix_pattern, *paths)
82
+ paths.collect! {|path| File.join(path, suffix_pattern) }
83
+ Dir[*paths].uniq
84
+ end
85
+
86
+ # Like Dir.chdir but makes the directory, if necessary, when mkdir is
87
+ # specified. chdir raises an error for non-existant directories, as well
88
+ # as non-directory inputs.
89
+ def chdir(dir, mkdir=false, &block)
90
+ dir = File.expand_path(dir)
91
+
92
+ unless File.directory?(dir)
93
+ if !File.exists?(dir) && mkdir
94
+ FileUtils.mkdir_p(dir)
95
+ else
96
+ raise ArgumentError, "not a directory: #{dir}"
97
+ end
98
+ end
99
+
100
+ Dir.chdir(dir, &block)
101
+ end
102
+
103
+ # Prepares the input path by making the parent directory for path. If a
104
+ # block is given, a file is created at path and passed to it; in this
105
+ # way files with non-existant parent directories are readily made.
106
+ #
107
+ # Returns path.
108
+ def prepare(path)
109
+ dirname = File.dirname(path)
110
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
111
+ File.open(path, "w") {|io| yield(io) } if block_given?
112
+ path
113
+ end
114
+
115
+ # The path root type indicating windows, *nix, or some unknown style of
116
+ # paths (:win, :nix, :unknown).
117
+ def path_root_type
118
+ @path_root_type ||= case
119
+ when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
120
+ when File.expand_path(".")[0] == ?/ then :nix
121
+ else :unknown
122
+ end
123
+ end
124
+
125
+ # Returns true if the input path appears to be an expanded path, based on
126
+ # path_root_type.
127
+ #
128
+ # If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
129
+ #
130
+ # expanded?('C:/path') # => true
131
+ # expanded?('c:/path') # => true
132
+ # expanded?('D:/path') # => true
133
+ # expanded?('path') # => false
134
+ #
135
+ # If root_type == :nix, then expanded? returns true if the path begins
136
+ # with '/'.
137
+ #
138
+ # expanded?('/path') # => true
139
+ # expanded?('path') # => false
140
+ #
141
+ # Otherwise expanded? always returns nil.
142
+ def expanded?(path, root_type=path_root_type)
143
+ case root_type
144
+ when :win
145
+ path =~ WIN_ROOT_PATTERN ? true : false
146
+ when :nix
147
+ path[0] == ?/
148
+ else
149
+ nil
150
+ end
151
+ end
152
+
153
+ # Returns true if path is relative to dir. Both path and dir will be
154
+ # expanded relative to dir_string, if specified.
155
+ def relative?(dir, path, dir_string=Dir.pwd)
156
+ if dir_string
157
+ dir = File.expand_path(dir, dir_string)
158
+ path = File.expand_path(path, dir_string)
159
+ end
160
+
161
+ path.rindex(dir, 0) == 0
162
+ end
163
+
164
+ # Trivial indicates when a path does not have content to load. Returns
165
+ # true if the file at path is empty, non-existant, a directory, or nil.
166
+ def trivial?(path)
167
+ path == nil || !File.file?(path) || File.size(path) == 0
168
+ end
169
+
170
+ # Empty returns true when dir is an existing directory that has no files.
171
+ def empty?(dir)
172
+ File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
173
+ end
174
+
175
+ # Returns the path segments for the given path, splitting along the path
176
+ # divider. Env paths are always represented by a string, if only an
177
+ # empty string.
178
+ #
179
+ # os divider example
180
+ # windows '\' split('C:\path\to\file') # => ["C:", "path", "to", "file"]
181
+ # *nix '/' split('/path/to/file') # => ["", "path", "to", "file"]
182
+ #
183
+ # The path is always expanded relative to the expand_dir; so '.' and
184
+ # '..' are resolved. However, unless expand_path == true, only the
185
+ # segments relative to the expand_dir are returned.
186
+ #
187
+ # On windows (note that expanding paths allows the use of slashes or
188
+ # backslashes):
189
+ #
190
+ # Dir.pwd # => 'C:/'
191
+ # split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
192
+ # split('path/to/.././to/file', false) # => ["path", "to", "file"]
193
+ #
194
+ # On *nix (or more generally systems with '/' roots):
195
+ #
196
+ # Dir.pwd # => '/'
197
+ # split('path/to/.././to/file') # => ["", "path", "to", "file"]
198
+ # split('path/to/.././to/file', false) # => ["path", "to", "file"]
199
+ #
200
+ def split(path, expand_path=true, expand_dir=Dir.pwd)
201
+ path = if expand_path
202
+ File.expand_path(path, expand_dir)
203
+ else
204
+ # normalize the path by expanding it, then
205
+ # work back to the relative path as needed
206
+ expanded_dir = File.expand_path(expand_dir)
207
+ expanded_path = File.expand_path(path, expand_dir)
208
+ expanded_path.index(expanded_dir) != 0 ? expanded_path : relative_path(expanded_dir, expanded_path)
209
+ end
210
+
211
+ segments = path.scan(/[^\/]+/)
212
+
213
+ # add back the root path as needed on *nix
214
+ segments.unshift "" if path[0] == ?/
215
+ segments
216
+ end
217
+
218
+ end
219
+ end
220
+ end
@@ -1,15 +1,14 @@
1
1
  module Tap
2
- module Support
3
-
2
+ class Root
4
3
  # Version provides methods for adding, removing, and incrementing versions
5
- # at the end of filepaths. Versions are all formatted like:
6
- # 'filepath-version.extension'.
4
+ # at the end of paths. Versions are all formatted like:
5
+ # 'path-version.extension'.
7
6
  #
8
7
  module Versions
9
-
10
- # Adds a version to the filepath. Versioned filepaths follow the format:
11
- # 'path-version.extension'. If no version is specified, then the filepath
12
- # is returned.
8
+
9
+ # Adds a version to the path. Versioned paths follow the format:
10
+ # 'path-version.extension'. If no version is specified, then the
11
+ # path is returned.
13
12
  #
14
13
  # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
15
14
  #
@@ -22,15 +21,15 @@ module Tap
22
21
  path.chomp(extname) + '-' + version + extname
23
22
  end
24
23
  end
25
-
26
- # Increments the version of the filepath by the specified increment.
24
+
25
+ # Increments the version of the path by the specified increment.
27
26
  #
28
27
  # increment("path/to/file-1.0.txt", "0.0.1") # => "path/to/file-1.0.1.txt"
29
28
  # increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
30
29
  #
31
30
  def increment(path, increment)
32
31
  path, version = deversion(path)
33
-
32
+
34
33
  # split the version and increment into integer arrays of equal length
35
34
  increment, version = [increment, version].collect do |vstr|
36
35
  begin
@@ -39,8 +38,11 @@ module Tap
39
38
  raise "Bad version or increment: #{vstr}"
40
39
  end
41
40
  end
42
- version.concat Array.new(increment.length - version.length, 0) if increment.length > version.length
43
-
41
+
42
+ if increment.length > version.length
43
+ version.concat Array.new(increment.length - version.length, 0)
44
+ end
45
+
44
46
  # add the increment to version
45
47
  0.upto(version.length-1) do |i|
46
48
  version[i] += (increment[i] || 0)
@@ -48,9 +50,10 @@ module Tap
48
50
 
49
51
  self.version(path, version.join("."))
50
52
  end
51
-
52
- # Splits the version from the input path, then returns the path and version.
53
- # If no version is specified, then the returned version will be nil.
53
+
54
+ # Splits the version from the input path, then returns the path and
55
+ # version. If no version is specified, then the returned version will be
56
+ # nil.
54
57
  #
55
58
  # deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
56
59
  # deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
@@ -60,8 +63,8 @@ module Tap
60
63
  extname = '' if extname =~ /^\.\d+$/
61
64
  path =~ /^(.*)-(\d(\.?\d)*)#{extname}$/ ? [$1 + extname, $2] : [path, nil]
62
65
  end
63
-
64
- # A <=> comparison for versions. compare_versions can take strings,
66
+
67
+ # A <=> comparison for versions. compare_versions can take strings,
65
68
  # integers, or even arrays representing the parts of a version.
66
69
  #
67
70
  # compare_versions("1.0.0", "0.9.9") # => 1
@@ -69,17 +72,17 @@ module Tap
69
72
  # compare_versions([0,9], [0,9,1]) # => -1
70
73
  def compare_versions(a,b)
71
74
  a, b = [a,b].collect {|item| to_integer_array(item) }
72
-
75
+
73
76
  # equalize the lengths of the integer arrays
74
77
  d = b.length - a.length
75
78
  case
76
79
  when d < 0 then b.concat Array.new(-d, 0)
77
80
  when d > 0 then a.concat Array.new(d, 0)
78
81
  end
79
-
82
+
80
83
  a <=> b
81
84
  end
82
-
85
+
83
86
  # Version unique. Select the latest or earliest versions of each file
84
87
  # in the array. For paths that have no version, vniq considers any
85
88
  # version to beat no version. The order of paths is preserved by
@@ -104,25 +107,25 @@ module Tap
104
107
  base, version = deversion(path)
105
108
  (unique[base] ||= []) << version
106
109
  end
107
-
110
+
108
111
  results = []
109
112
  unique.each_pair do |base, versions|
110
113
  versions = versions.sort {|a, b| compare_versions(a,b) }
111
114
  winner = earliest ? versions.shift : versions.pop
112
115
  results << version(base, winner)
113
116
  end
114
-
117
+
115
118
  results = results.sort_by do |path|
116
119
  array.index(path)
117
120
  end if preserve_order
118
-
121
+
119
122
  results
120
123
  end
121
-
124
+
122
125
  private
123
-
124
- # Converts an input argument (typically a string or an array)
125
- # to an array of integers. Splits version string on "."
126
+
127
+ # Converts an input argument (typically a string or an array) to an
128
+ # array of integers. Splits version string on "."
126
129
  def to_integer_array(arg)
127
130
  arr = case arg
128
131
  when Array then arg
@@ -130,7 +133,6 @@ module Tap
130
133
  end
131
134
  arr.collect {|i| i.to_i}
132
135
  end
133
-
134
136
  end
135
137
  end
136
138
  end
data/lib/tap/schema.rb ADDED
@@ -0,0 +1,248 @@
1
+ require 'tap/schema/utils'
2
+ require 'tap/schema/parser'
3
+
4
+ module Tap
5
+ class Schema
6
+ class << self
7
+ def load(str)
8
+ new(YAML.load(str) || {})
9
+ end
10
+
11
+ def load_file(path)
12
+ load(File.read(path))
13
+ end
14
+ end
15
+
16
+ include Utils
17
+
18
+ # A hash of task schema describing individual tasks in a workflow. Tasks
19
+ # only require a class, but may contain configurations and even arguments
20
+ # for enque. Individual tasks may be a hash or an array. The tasks are
21
+ # resolved if they take one of these forms:
22
+ #
23
+ # tasks:
24
+ # key: {:class: TaskClass, ...}
25
+ # key: [TaskClass, ...]
26
+ #
27
+ attr_reader :tasks
28
+
29
+ # An array of join schema that describe how to join tasks together. Joins
30
+ # have arrays of inputs and outputs that reference task keys. Individual
31
+ # joins may be a hash or an array. The joins are resolved if they take
32
+ # one of these forms:
33
+ #
34
+ # joins:
35
+ # - [[inputs], [outputs], {:class: JoinClass, ...}]
36
+ # - [[inputs], [outputs], [JoinClass, ...]]
37
+ #
38
+ attr_reader :joins
39
+
40
+ # An array of [key, [args]] data that indicates the tasks and arguments
41
+ # to be added to an application during build. If args are not specified,
42
+ # then the arguments specified in the task schema are used.
43
+ #
44
+ # queue:
45
+ # - key # uses tasks[key] arguments
46
+ # - [key, [1, 2, 3]] # enques tasks[key] with [1, 2, 3]
47
+ #
48
+ attr_reader :queue
49
+
50
+ # An array of middleware to build onto the app.
51
+ attr_reader :middleware
52
+
53
+ def initialize(schema={})
54
+ @tasks = hashify(schema['tasks'] || {})
55
+
56
+ @joins = dehashify(schema['joins'] || []).collect do |join|
57
+ inputs, outputs, join = dehashify(join)
58
+ [inputs, outputs, join]
59
+ end
60
+
61
+ @queue = dehashify(schema['queue'] || []).collect do |queue|
62
+ dehashify(queue)
63
+ end
64
+
65
+ @middleware = dehashify(schema['middleware'] || [])
66
+ end
67
+
68
+ def resolve!
69
+ tasks.dup.each_pair do |key, task|
70
+ task ||= {}
71
+ tasks[key] = resolve(task) do |id, data|
72
+ yield(:task, id || key, data)
73
+ end
74
+ end
75
+
76
+ joins.collect! do |inputs, outputs, join|
77
+ join ||= {}
78
+ join = resolve(join) do |id, data|
79
+ yield(:join, id || 'join', data)
80
+ end
81
+ [inputs, outputs, join]
82
+ end
83
+
84
+ middleware.collect! do |m|
85
+ resolve(m) do |id, data|
86
+ yield(:middleware, id, data)
87
+ end
88
+ end
89
+
90
+ self
91
+ end
92
+
93
+ def validate!
94
+ errors = []
95
+ tasks.each do |key, task|
96
+ unless resolved?(task)
97
+ errors << "unknown task: #{task}"
98
+ end
99
+ end
100
+
101
+ joins.each do |inputs, outputs, join|
102
+ unless resolved?(join)
103
+ errors << "unknown join: #{join}"
104
+ end
105
+
106
+ inputs.each do |key|
107
+ unless tasks.has_key?(key)
108
+ errors << "missing join input: #{key}"
109
+ end
110
+ end
111
+
112
+ outputs.each do |key|
113
+ unless tasks.has_key?(key)
114
+ errors << "missing join output: #{key}"
115
+ end
116
+ end
117
+ end
118
+
119
+ # queue.each do |(key, args)|
120
+ # unless tasks.has_key?(key)
121
+ # errors << "missing task: #{key}"
122
+ # end
123
+ # end
124
+
125
+ middleware.each do |m|
126
+ unless resolved?(m)
127
+ errors << "unknown middleware: #{m}"
128
+ end
129
+ end
130
+
131
+ unless errors.empty?
132
+ prefix = if errors.length > 1
133
+ "#{errors.length} build errors\n"
134
+ else
135
+ ""
136
+ end
137
+
138
+ raise "#{prefix}#{errors.join("\n")}\n"
139
+ end
140
+
141
+ self
142
+ end
143
+
144
+ def cleanup!
145
+ joins.delete_if do |inputs, outputs, join|
146
+
147
+ # remove missing inputs
148
+ inputs.delete_if {|key| !tasks.has_key?(key) }
149
+
150
+ # remove missing outputs
151
+ outputs.delete_if {|key| !tasks.has_key?(key) }
152
+
153
+ # remove orphan joins
154
+ inputs.empty? || outputs.empty?
155
+ end
156
+
157
+ # remove inputs without a task
158
+ queue.delete_if do |(key, inputs)|
159
+ !tasks.has_key?(key)
160
+ end
161
+
162
+ self
163
+ end
164
+
165
+ def scrub!
166
+ tasks.each do |key, task|
167
+ yield(task)
168
+ end
169
+
170
+ joins.each do |inputs, outputs, join|
171
+ yield(join)
172
+ end
173
+ end
174
+
175
+ def build(app)
176
+ # instantiate tasks
177
+ tasks = {}
178
+ arguments = {}
179
+ self.tasks.each_pair do |key, task|
180
+ instance, args = instantiate(task, app)
181
+
182
+ tasks[key] = instance
183
+ arguments[key] = args
184
+ end
185
+
186
+ # build the workflow
187
+ self.joins.each do |inputs, outputs, join|
188
+ inputs = inputs.collect {|key| tasks[key] }
189
+ outputs = outputs.collect {|key| tasks[key] }
190
+ instantiate(join, app).join(inputs, outputs)
191
+ end
192
+
193
+ # utilize middleware
194
+ self.middleware.each do |middleware|
195
+ instantiate(middleware, app)
196
+ end
197
+
198
+ # enque tasks
199
+ queue.each do |(key, inputs)|
200
+ unless inputs
201
+ inputs = arguments[key]
202
+ end
203
+
204
+ app.enq(tasks[key], *inputs) if inputs
205
+ end
206
+
207
+ tasks
208
+ end
209
+
210
+ def traverse
211
+ map = {}
212
+ self.tasks.each_pair do |key, task|
213
+ map[key] = [[],[]]
214
+ end
215
+
216
+ index = 0
217
+ self.joins.each do |inputs, outputs, join|
218
+ inputs.each do |key|
219
+ map[key][1] << index
220
+ end
221
+
222
+ outputs.each do |key|
223
+ map[key][0] << index
224
+ end
225
+
226
+ index += 1
227
+ end
228
+
229
+ map.keys.sort.collect do |key|
230
+ [key, *map[key]]
231
+ end
232
+ end
233
+
234
+ # Creates an hash dump of self.
235
+ def to_hash
236
+ { 'tasks' => tasks,
237
+ 'joins' => joins,
238
+ 'queue' => queue,
239
+ 'middleware' => middleware
240
+ }
241
+ end
242
+
243
+ # Converts self to a hash and serializes it to YAML.
244
+ def dump(io=nil)
245
+ YAML.dump(to_hash, io)
246
+ end
247
+ end
248
+ end