tap 0.19.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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