tap 0.12.4 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History +34 -0
- data/README +62 -41
- data/bin/tap +36 -40
- data/cmd/console.rb +14 -6
- data/cmd/manifest.rb +62 -58
- data/cmd/run.rb +49 -31
- data/doc/API +84 -0
- data/doc/Class Reference +83 -115
- data/doc/Examples/Command Line +36 -0
- data/doc/Examples/Workflow +40 -0
- data/lib/tap/app.rb +293 -214
- data/lib/tap/app/node.rb +43 -0
- data/lib/tap/app/queue.rb +77 -0
- data/lib/tap/app/stack.rb +16 -0
- data/lib/tap/app/state.rb +22 -0
- data/lib/tap/constants.rb +2 -2
- data/lib/tap/env.rb +400 -314
- data/lib/tap/env/constant.rb +227 -0
- data/lib/tap/env/gems.rb +63 -0
- data/lib/tap/env/manifest.rb +89 -0
- data/lib/tap/env/minimap.rb +292 -0
- data/lib/tap/{support → env}/string_ext.rb +2 -2
- data/lib/tap/exe.rb +113 -125
- data/lib/tap/join.rb +175 -0
- data/lib/tap/joins.rb +9 -0
- data/lib/tap/joins/switch.rb +44 -0
- data/lib/tap/joins/sync.rb +99 -0
- data/lib/tap/root.rb +100 -491
- data/lib/tap/root/utils.rb +220 -0
- data/lib/tap/{support → root}/versions.rb +31 -29
- data/lib/tap/schema.rb +248 -0
- data/lib/tap/schema/parser.rb +413 -0
- data/lib/tap/schema/utils.rb +82 -0
- data/lib/tap/support/intern.rb +19 -6
- data/lib/tap/support/templater.rb +8 -3
- data/lib/tap/task.rb +175 -171
- data/lib/tap/tasks/dump.rb +58 -0
- data/lib/tap/tasks/load.rb +62 -0
- metadata +30 -73
- data/cmd/destroy.rb +0 -27
- data/cmd/generate.rb +0 -27
- data/doc/Command Reference +0 -105
- data/doc/Syntax Reference +0 -234
- data/doc/Tutorial +0 -348
- data/lib/tap/dump.rb +0 -142
- data/lib/tap/file_task.rb +0 -384
- data/lib/tap/generator/arguments.rb +0 -13
- data/lib/tap/generator/base.rb +0 -176
- data/lib/tap/generator/destroy.rb +0 -60
- data/lib/tap/generator/generate.rb +0 -93
- data/lib/tap/generator/generators/command/command_generator.rb +0 -21
- data/lib/tap/generator/generators/command/templates/command.erb +0 -32
- data/lib/tap/generator/generators/config/config_generator.rb +0 -98
- data/lib/tap/generator/generators/generator/generator_generator.rb +0 -37
- data/lib/tap/generator/generators/generator/templates/task.erb +0 -27
- data/lib/tap/generator/generators/generator/templates/test.erb +0 -26
- data/lib/tap/generator/generators/root/root_generator.rb +0 -84
- data/lib/tap/generator/generators/root/templates/MIT-LICENSE +0 -22
- data/lib/tap/generator/generators/root/templates/README +0 -14
- data/lib/tap/generator/generators/root/templates/Rakefile +0 -84
- data/lib/tap/generator/generators/root/templates/Rapfile +0 -11
- data/lib/tap/generator/generators/root/templates/gemspec +0 -27
- data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -3
- data/lib/tap/generator/generators/task/task_generator.rb +0 -25
- data/lib/tap/generator/generators/task/templates/task.erb +0 -14
- data/lib/tap/generator/generators/task/templates/test.erb +0 -19
- data/lib/tap/generator/manifest.rb +0 -20
- data/lib/tap/generator/preview.rb +0 -69
- data/lib/tap/load.rb +0 -64
- data/lib/tap/spec.rb +0 -41
- data/lib/tap/support/aggregator.rb +0 -65
- data/lib/tap/support/audit.rb +0 -333
- data/lib/tap/support/constant.rb +0 -143
- data/lib/tap/support/constant_manifest.rb +0 -126
- data/lib/tap/support/dependencies.rb +0 -54
- data/lib/tap/support/dependency.rb +0 -44
- data/lib/tap/support/executable.rb +0 -198
- data/lib/tap/support/executable_queue.rb +0 -125
- data/lib/tap/support/gems.rb +0 -43
- data/lib/tap/support/join.rb +0 -144
- data/lib/tap/support/joins.rb +0 -12
- data/lib/tap/support/joins/switch.rb +0 -27
- data/lib/tap/support/joins/sync_merge.rb +0 -38
- data/lib/tap/support/manifest.rb +0 -171
- data/lib/tap/support/minimap.rb +0 -90
- data/lib/tap/support/node.rb +0 -176
- data/lib/tap/support/parser.rb +0 -450
- data/lib/tap/support/schema.rb +0 -385
- data/lib/tap/support/shell_utils.rb +0 -67
- data/lib/tap/test.rb +0 -77
- data/lib/tap/test/assertions.rb +0 -38
- data/lib/tap/test/env_vars.rb +0 -29
- data/lib/tap/test/extensions.rb +0 -73
- data/lib/tap/test/file_test.rb +0 -362
- data/lib/tap/test/file_test_class.rb +0 -15
- data/lib/tap/test/regexp_escape.rb +0 -87
- data/lib/tap/test/script_test.rb +0 -46
- data/lib/tap/test/script_tester.rb +0 -115
- data/lib/tap/test/subset_test.rb +0 -260
- data/lib/tap/test/subset_test_class.rb +0 -99
- data/lib/tap/test/tap_test.rb +0 -109
- 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
|
-
|
3
|
-
|
2
|
+
class Root
|
4
3
|
# Version provides methods for adding, removing, and incrementing versions
|
5
|
-
# at the end of
|
6
|
-
# '
|
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
|
11
|
-
# 'path-version.extension'. If no version is specified, then the
|
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
|
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
|
-
|
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
|
53
|
-
# If no version is specified, then the returned version will be
|
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
|
-
#
|
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
|