tap 0.19.0 → 1.3.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 (66) hide show
  1. data/History +100 -45
  2. data/MIT-LICENSE +1 -1
  3. data/README +95 -51
  4. data/bin/tap +11 -57
  5. data/bin/tapexe +84 -0
  6. data/doc/API +91 -139
  7. data/doc/Configuration +93 -0
  8. data/doc/Examples/Command Line +10 -42
  9. data/doc/Examples/Tapfile +124 -0
  10. data/doc/Ruby to Ruby +87 -0
  11. data/doc/Workflow Syntax +185 -0
  12. data/lib/tap.rb +74 -5
  13. data/lib/tap/app.rb +217 -310
  14. data/lib/tap/app/api.rb +44 -23
  15. data/lib/tap/app/queue.rb +11 -12
  16. data/lib/tap/app/stack.rb +4 -4
  17. data/lib/tap/declarations.rb +200 -0
  18. data/lib/tap/declarations/context.rb +31 -0
  19. data/lib/tap/declarations/description.rb +33 -0
  20. data/lib/tap/env.rb +133 -779
  21. data/lib/tap/env/cache.rb +87 -0
  22. data/lib/tap/env/constant.rb +94 -39
  23. data/lib/tap/env/path.rb +71 -0
  24. data/lib/tap/join.rb +42 -78
  25. data/lib/tap/joins/gate.rb +85 -0
  26. data/lib/tap/joins/switch.rb +4 -2
  27. data/lib/tap/joins/sync.rb +3 -3
  28. data/lib/tap/middleware.rb +5 -5
  29. data/lib/tap/middlewares/debugger.rb +18 -58
  30. data/lib/tap/parser.rb +115 -183
  31. data/lib/tap/root.rb +162 -239
  32. data/lib/tap/signal.rb +72 -0
  33. data/lib/tap/signals.rb +20 -2
  34. data/lib/tap/signals/class_methods.rb +38 -43
  35. data/lib/tap/signals/configure.rb +19 -0
  36. data/lib/tap/signals/help.rb +5 -7
  37. data/lib/tap/signals/load.rb +49 -0
  38. data/lib/tap/signals/module_methods.rb +1 -0
  39. data/lib/tap/task.rb +46 -275
  40. data/lib/tap/tasks/dump.rb +21 -16
  41. data/lib/tap/tasks/list.rb +184 -0
  42. data/lib/tap/tasks/load.rb +4 -4
  43. data/lib/tap/tasks/prompt.rb +128 -0
  44. data/lib/tap/tasks/signal.rb +42 -0
  45. data/lib/tap/tasks/singleton.rb +35 -0
  46. data/lib/tap/tasks/stream.rb +64 -0
  47. data/lib/tap/utils.rb +83 -0
  48. data/lib/tap/version.rb +2 -2
  49. data/lib/tap/workflow.rb +124 -0
  50. data/tap.yml +0 -0
  51. metadata +59 -24
  52. data/cmd/console.rb +0 -43
  53. data/cmd/manifest.rb +0 -118
  54. data/cmd/run.rb +0 -145
  55. data/doc/Examples/Workflow +0 -40
  56. data/lib/tap/app/node.rb +0 -29
  57. data/lib/tap/env/context.rb +0 -61
  58. data/lib/tap/env/gems.rb +0 -63
  59. data/lib/tap/env/manifest.rb +0 -179
  60. data/lib/tap/env/minimap.rb +0 -308
  61. data/lib/tap/intern.rb +0 -50
  62. data/lib/tap/joins.rb +0 -9
  63. data/lib/tap/prompt.rb +0 -36
  64. data/lib/tap/root/utils.rb +0 -220
  65. data/lib/tap/root/versions.rb +0 -138
  66. data/lib/tap/signals/signal.rb +0 -68
@@ -1,268 +1,191 @@
1
- require 'configurable'
2
- require 'tap/root/utils'
1
+ autoload(:FileUtils, 'fileutils')
3
2
 
4
3
  module Tap
5
-
6
- # Root abstracts a directory to standardize access to resources organized
7
- # within variable directory structures.
8
- #
9
- # # define a root directory with aliased relative paths
10
- # root = Root.new(
11
- # :root => '/root_dir',
12
- # :relative_paths => {:input => 'in', :output => 'out'})
13
- #
14
- # # access aliased paths
15
- # root[:input] # => '/root_dir/in'
16
- # root[:output] # => '/root_dir/out'
17
- # root['implicit'] # => '/root_dir/implicit'
18
- #
19
- # # absolute paths can also be aliased
20
- # root[:abs, true] = "/absolute/path"
21
- # root.path(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
22
- #
23
- # # expanded paths are returned unchanged
24
- # path = File.expand_path('expanded')
25
- # root[path] # => path
26
- #
27
- # # work with paths
28
- # path = root.path(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
29
- # root.relative_path(:input, path) # => 'path/to/file.txt'
30
- # root.translate(path, :input, :output) # => '/root_dir/out/path/to/file.txt'
31
- #
32
- # By default, Roots are initialized to the present working directory
33
- # (Dir.pwd).
34
- #
35
- # === Implementation Notes
36
- #
37
- # Internally Root expands and stores all aliased paths in the 'paths' hash.
38
- # Expanding paths ensures they remain constant even when the present working
39
- # directory (Dir.pwd) changes.
40
- #
41
- # Root keeps a separate 'relative_paths' hash mapping aliases to their
42
- # relative paths. This hash allow reassignment if and when the root directory
43
- # changes. By contrast, there is no separate data structure storing the
44
- # absolute paths. An absolute path thus has an alias in 'paths' but not
45
- # 'relative_paths', whereas relative paths have aliases in both.
46
- #
47
- # These features may be important to note when subclassing Root:
48
- # - root and all paths in 'paths' are expanded
49
- # - relative paths are stored in 'relative_paths'
50
- # - absolute paths are present in 'paths' but not in 'relative_paths'
51
- #
52
4
  class Root
53
- include Configurable
54
- include Utils
55
-
56
- # The root directory.
57
- config_attr(:root, '.', :writer => false, :init => false)
58
-
59
- # A hash of (alias, relative path) pairs for aliased paths relative
60
- # to root.
61
- config_attr(:relative_paths, {}, :writer => false, :init => false, :type => :hash)
62
-
63
- # A hash of (alias, relative path) pairs for aliased absolute paths.
64
- config_attr(:absolute_paths, {}, :reader => false, :writer => false, :init => false, :type => :hash)
65
-
66
- # A hash of (alias, expanded path) pairs for expanded relative and
67
- # absolute paths.
68
- attr_reader :paths
69
-
70
- # The filesystem root, inferred from self.root
71
- # (ex '/' on *nix or something like 'C:/' on Windows).
72
- attr_reader :path_root
73
-
74
- # Creates a new Root from the specified configurations. A directory may be
75
- # provided instead of a configuration hash; in that case no aliased relative
76
- # or absolute paths are specified. By default root is the present working
77
- # directory.
78
- def initialize(config_or_dir=Dir.pwd)
79
- # root, relative_paths, and absolute_paths are assigned manually as
80
- # an optimization (otherwise assign_paths would get called once for
81
- # each configuration)
82
- if config_or_dir.kind_of?(String)
83
- assign_paths(config_or_dir, {}, {})
84
- config_or_dir = {}
85
- else
86
- root = config_or_dir.delete(:root) || Dir.pwd
87
- relative_paths = config_or_dir.delete(:relative_paths) || {}
88
- absolute_paths = config_or_dir.delete(:absolute_paths) || {}
89
- assign_paths(root, relative_paths, absolute_paths)
5
+ class << self
6
+ # The path root type indicating windows, *nix, or some unknown style of
7
+ # paths (:win, :nix, :unknown).
8
+ def type
9
+ @path_root_type ||= begin
10
+ pwd = File.expand_path('.')
11
+
12
+ case
13
+ when pwd =~ WIN_ROOT_PATTERN then :win
14
+ when pwd[0] == ?/ then :nix
15
+ else :unknown
16
+ end
17
+ end
18
+ end
19
+
20
+ # Trivial indicates when a path does not have content to load. Returns
21
+ # true if the file at path is empty, non-existant, a directory, or nil.
22
+ def trivial?(path)
23
+ path == nil || !File.file?(path) || File.size(path) == 0
90
24
  end
91
25
 
92
- initialize_config(config_or_dir)
93
- end
94
-
95
- # Sets the root directory. All paths are reassigned accordingly.
96
- def root=(path)
97
- assign_paths(path, relative_paths, absolute_paths)
98
- end
99
-
100
- # Sets the relative_paths to those provided. 'root' and :root are reserved
101
- # aliases and cannot be set using this method (use root= instead).
102
- #
103
- # r = Root.new
104
- # r['alt'] # => File.join(r.root, 'alt')
105
- # r.relative_paths = {'alt' => 'dir'}
106
- # r['alt'] # => File.join(r.root, 'dir')
107
- #
108
- def relative_paths=(paths)
109
- paths = Validation::HASH[paths]
110
- assign_paths(root, paths, absolute_paths)
26
+ # Empty returns true when dir is an existing directory that has no files.
27
+ def empty?(dir)
28
+ File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
29
+ end
111
30
  end
112
-
113
- # Sets the absolute paths to those provided. 'root' and :root are reserved
114
- # aliases and cannot be set using this method (use root= instead).
115
- #
116
- # r = Root.new
117
- # r['abs'] # => File.join(r.root, 'abs')
118
- # r.absolute_paths = {'abs' => '/path/to/dir'}
119
- # r['abs'] # => '/path/to/dir'
120
- #
121
- def absolute_paths=(paths)
122
- paths = Validation::HASH[paths]
123
- assign_paths(root, relative_paths, paths)
31
+
32
+ # Regexp to match a windows-style root path.
33
+ WIN_ROOT_PATTERN = /\A[A-z]:\//
34
+
35
+ def initialize(path=Dir.pwd, dir=Dir.pwd)
36
+ @path_root = File.expand_path(path.to_s, dir.to_s)
124
37
  end
125
-
126
- # Returns the absolute paths registered with self.
127
- def absolute_paths
128
- abs_paths = {}
129
- paths.each do |als, path|
130
- unless relative_paths.include?(als) || als.to_s == 'root'
131
- abs_paths[als] = path
132
- end
133
- end
134
- abs_paths
38
+
39
+ # Stringifies and expands the path relative to self. Paths are turned
40
+ # into strings using to_s.
41
+ def expand(path)
42
+ File.expand_path(path.to_s, @path_root)
135
43
  end
136
-
137
- # Sets an alias for the path relative to the root directory. The aliases
138
- # 'root' and :root cannot be set with this method (use root= instead).
139
- # Absolute paths can be set using the second syntax.
140
- #
141
- # r = Root.new '/root_dir'
142
- # r[:dir] = 'path/to/dir'
143
- # r[:dir] # => '/root_dir/path/to/dir'
144
- #
145
- # r[:abs, true] = '/abs/path/to/dir'
146
- # r[:abs] # => '/abs/path/to/dir'
147
- #
148
- #--
149
- # Implementation Note:
150
- #
151
- # The syntax for setting an absolute path requires an odd use []=.
152
- # In fact the method receives the arguments (:dir, true, '/abs/path/to/dir')
153
- # rather than (:dir, '/abs/path/to/dir', true) meaning that internally path
154
- # and absolute are switched when setting an absolute path.
155
- #
156
- def []=(als, path, absolute=false)
157
- raise ArgumentError, "the alias #{als.inspect} is reserved" if als.to_s == 'root'
158
-
159
- # switch the paths if absolute was provided
160
- unless absolute == false
161
- path, absolute = absolute, path
162
- end
163
44
 
164
- case
165
- when path.nil?
166
- @relative_paths.delete(als)
167
- @paths.delete(als)
168
- when absolute
169
- @relative_paths.delete(als)
170
- @paths[als] = File.expand_path(path)
171
- else
172
- @relative_paths[als] = path
173
- @paths[als] = File.expand_path(File.join(root, path))
174
- end
45
+ # Joins and expands the path segments relative to self. Segments are
46
+ # turned to strings using to_s.
47
+ def path(*segments)
48
+ segments.collect! {|seg| seg.to_s }
49
+ expand(File.join(*segments))
175
50
  end
176
-
177
- # Returns the expanded path for the specified alias. If the alias has not
178
- # been set, then the path is inferred to be 'root/als'. Expanded paths
179
- # are returned directly.
180
- #
181
- # r = Root.new '/root_dir', :dir => 'path/to/dir'
182
- # r[:dir] # => '/root_dir/path/to/dir'
183
- #
184
- # r.path_root # => '/'
185
- # r['relative/path'] # => '/root_dir/relative/path'
186
- # r['/expanded/path'] # => '/expanded/path'
187
- #
188
- def [](als)
189
- path = self.paths[als]
190
- return path unless path == nil
191
51
 
192
- als = als.to_s
193
- expanded?(als) ? als : File.expand_path(File.join(root, als))
52
+ # Returns true if the expanded path is relative to self.
53
+ def relative?(path)
54
+ expand(path).rindex(@path_root, 0) == 0
194
55
  end
195
56
 
196
- # Resolves the specified alias, joins paths together, and expands the
197
- # resulting path.
198
- def path(als, *paths)
199
- File.expand_path(File.join(self[als], *paths))
200
- end
57
+ # Returns the part of the expanded path relative to self, or nil if the
58
+ # path is not relative to self.
59
+ def relative_path(path)
60
+ path = expand(path)
61
+ return nil unless relative?(path)
201
62
 
202
- # Returns true if the path is relative to the specified alias.
203
- def relative?(als, path)
204
- super(self[als], path)
63
+ # if path_root_length > path.length then the first arg
64
+ # returns nil, and an empty string is returned
65
+ path[path_root_length, path.length - path_root_length] || ""
205
66
  end
67
+ alias rp relative_path
206
68
 
207
- # Returns the part of path relative to the specified alias.
208
- def relative_path(als, path)
209
- super(self[als], path)
69
+ # Returns a new Root for the path, relative to self.
70
+ def root(path)
71
+ Root.new(path, self)
72
+ end
73
+
74
+ # Returns a new Root for the path, relative to self. Same as root but
75
+ # raises an error if the path is not relative to self.
76
+ def sub(path)
77
+ sub = root(path)
78
+ unless relative?(sub)
79
+ raise ArgumentError, "not a sub path: #{sub} (#{self})"
80
+ end
81
+ sub
82
+ end
83
+
84
+ # Returns a new Root for the parent directory for self.
85
+ def parent
86
+ root File.dirname(@path_root)
210
87
  end
211
88
 
212
- # Generates a path translated from the aliased source to the aliased target.
213
- # Raises an error if path is not relative to the source.
214
- #
215
- # r = Root.new '/root_dir'
216
- # path = r.path(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
217
- # r.translate(path, :in, :out) # => '/root_dir/out/path/to/file.txt'
89
+ # Returns the expanded path, exchanging the extension with extname.
90
+ # Extname may optionally omit the leading period.
218
91
  #
219
- def translate(path, source_als, target_als)
220
- super(path, self[source_als], self[target_als])
92
+ # root = Root.new("/root")
93
+ # root.exchange('path/to/file.txt', '.html') # => '/root/path/to/file.html'
94
+ # root.exchange('path/to/file.txt', 'rb') # => '/root/path/to/file.rb'
95
+ #
96
+ def exchange(path, extname)
97
+ path = expand(path)
98
+ "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
221
99
  end
222
-
223
- # Globs for paths along the aliased path matching the input patterns.
224
- # Patterns should join with the aliased path make valid globs for
225
- # Dir.glob. If no patterns are specified, glob returns all paths
226
- # matching 'als/**/*'.
227
- def glob(als, *patterns)
100
+ alias ex exchange
101
+
102
+ # Generates a path translated from the source to the target. Raises an
103
+ # error if path is not relative to the source.
104
+ def translate(path, source, target)
105
+ path = expand(path)
106
+ source = root(source)
107
+ target = root(target)
108
+
109
+ rp = source.relative_path(path)
110
+ if rp.nil?
111
+ raise ArgumentError, "\n#{path}\nis not relative to:\n#{source}"
112
+ end
113
+
114
+ target.path(rp)
115
+ end
116
+ alias tr translate
117
+
118
+ # Globs for unique paths matching the input patterns expanded relative to
119
+ # self. If no patterns are specified, glob returns all paths matching
120
+ # './**/*'.
121
+ def glob(*patterns)
228
122
  patterns << "**/*" if patterns.empty?
229
- patterns.collect! {|pattern| path(als, pattern)}
230
- super(*patterns)
123
+ patterns.collect! {|pattern| expand(pattern) }
124
+ Dir[*patterns].uniq
231
125
  end
232
-
233
- # Lists all versions of path in the aliased dir matching the version
234
- # patterns. If no patterns are specified, then all versions of path
235
- # will be returned.
236
- def version_glob(als, path, *vpatterns)
237
- super(path(als, path), *vpatterns)
126
+
127
+ # Changes to the specified directory using Dir.chdir, keeping the same
128
+ # block semantics as that method. The directory will be created if
129
+ # necessary and mkdir is specified. Raises an error for non-existant
130
+ # directories, as well as non-directory inputs.
131
+ def chdir(dir, mkdir=false, &block)
132
+ dir = expand(dir)
133
+
134
+ unless File.directory?(dir)
135
+ if !File.exists?(dir) && mkdir
136
+ FileUtils.mkdir_p(dir)
137
+ else
138
+ raise ArgumentError, "not a directory: #{dir}"
139
+ end
140
+ end
141
+
142
+ Dir.chdir(dir, &block)
238
143
  end
239
-
240
- # Changes pwd to the specified directory using Root.chdir.
241
- def chdir(als, mkdir=false, &block)
242
- super(self[als], mkdir, &block)
144
+
145
+ # Makes the specified directory and parent directories (as required).
146
+ def mkdir(*path)
147
+ path = self.path(*path)
148
+ FileUtils.mkdir_p(path) unless File.directory?(path)
149
+ path
243
150
  end
244
-
245
- # Constructs a path from the inputs and prepares it using
246
- # Root.prepare. Returns the path.
247
- def prepare(als, *paths, &block)
248
- super(path(als, *paths), &block)
151
+
152
+ # Opens the path in the specified mode, using the same semantics as
153
+ # File.open.
154
+ def open(path, mode='r', &block)
155
+ path = expand(path)
156
+ File.open(path, mode, &block)
249
157
  end
250
-
158
+
159
+ # Prepares a file at the path by making paths's parent directory. The file
160
+ # is opened in the mode and passed to the block, if given. The mode is
161
+ # ignored if no block is given.
162
+ #
163
+ # Returns path.
164
+ def prepare(*path)
165
+ path = self.path(*path)
166
+ dirname = File.dirname(path)
167
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
168
+ File.open(path, 'w') {|io| yield(io) } if block_given?
169
+ path
170
+ end
171
+
172
+ # Returns path.
173
+ def to_s
174
+ @path_root
175
+ end
176
+
251
177
  private
252
-
253
- # reassigns all paths with the input root, relative_paths, and absolute_paths
254
- def assign_paths(root, relative_paths, absolute_paths) # :nodoc:
255
- @root = File.expand_path(root)
256
- @relative_paths = {}
257
- @paths = {'root' => @root, :root => @root}
258
-
259
- @path_root = File.dirname(@root)
260
- while @path_root != (parent = File.dirname(@path_root))
261
- @path_root = parent
178
+
179
+ # helper to memoize and return the length of path root, plus a trailing
180
+ # separator; used in determining relative paths
181
+ def path_root_length # :nodoc:
182
+ @path_root_length ||= begin
183
+ length = @path_root.length
184
+ unless @path_root == File::SEPARATOR
185
+ length += File::SEPARATOR.length
186
+ end
187
+ length
262
188
  end
263
-
264
- relative_paths.each_pair {|dir, path| self[dir] = path }
265
- absolute_paths.each_pair {|dir, path| self[dir, true] = path }
266
- end
189
+ end
267
190
  end
268
191
  end
@@ -0,0 +1,72 @@
1
+ require 'tap/utils'
2
+
3
+ module Tap
4
+ # Signal attaches an object and allows a specific method to be triggered
5
+ # through a standard interface.
6
+ class Signal
7
+ class << self
8
+ # A description of self
9
+ attr_accessor :desc
10
+ end
11
+
12
+ # The object receiving signals through self.
13
+ attr_reader :obj
14
+
15
+ # An optional block, used at the signal's discretion (normally passed to
16
+ # the method the signal targets on obj).
17
+ attr_reader :block
18
+
19
+ def initialize(obj, &block)
20
+ @obj = obj
21
+ @block = block
22
+ end
23
+
24
+ # Calls process with the input args and returns the result.
25
+ def call(args)
26
+ process(args)
27
+ end
28
+
29
+ # Simply returns the input args.
30
+ def process(args)
31
+ args
32
+ end
33
+
34
+ def inspect
35
+ "#<#{self.class}:#{object_id}>"
36
+ end
37
+
38
+ protected
39
+
40
+ def convert_to_array(obj, signature=[], options=false)
41
+ return obj if obj.kind_of?(Array)
42
+
43
+ argv = signature.collect {|key| obj[key] }
44
+
45
+ if options
46
+ opts = {}
47
+ (obj.keys - signature).each do |key|
48
+ opts[key] = obj[key]
49
+ end
50
+
51
+ argv << opts
52
+ end
53
+
54
+ argv
55
+ end
56
+
57
+ def convert_to_hash(obj, signature=[], remainder=nil)
58
+ return obj if obj.kind_of?(Hash)
59
+
60
+ args, argh = obj, {}
61
+ signature.each do |key|
62
+ argh[key] = args.shift
63
+ end
64
+
65
+ if remainder
66
+ argh[remainder] = args
67
+ end
68
+
69
+ argh
70
+ end
71
+ end
72
+ end