tap 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. data/History +35 -1
  2. data/MIT-LICENSE +1 -1
  3. data/README +16 -15
  4. data/bin/tap +1 -1
  5. data/cmd/console.rb +4 -3
  6. data/cmd/manifest.rb +2 -2
  7. data/cmd/run.rb +12 -15
  8. data/doc/Class Reference +120 -117
  9. data/doc/Command Reference +27 -27
  10. data/doc/Syntax Reference +55 -111
  11. data/doc/Tutorial +69 -26
  12. data/lib/tap.rb +3 -8
  13. data/lib/tap/app.rb +122 -146
  14. data/lib/tap/constants.rb +2 -2
  15. data/lib/tap/env.rb +178 -252
  16. data/lib/tap/exe.rb +67 -30
  17. data/lib/tap/file_task.rb +224 -411
  18. data/lib/tap/generator/arguments.rb +13 -0
  19. data/lib/tap/generator/base.rb +112 -30
  20. data/lib/tap/generator/destroy.rb +36 -13
  21. data/lib/tap/generator/generate.rb +69 -48
  22. data/lib/tap/generator/generators/command/templates/command.erb +3 -3
  23. data/lib/tap/generator/generators/config/config_generator.rb +82 -10
  24. data/lib/tap/generator/generators/generator/generator_generator.rb +16 -6
  25. data/lib/tap/generator/generators/generator/templates/task.erb +2 -2
  26. data/lib/tap/generator/generators/generator/templates/test.erb +26 -0
  27. data/lib/tap/generator/generators/root/root_generator.rb +24 -13
  28. data/lib/tap/generator/generators/root/templates/Rakefile +4 -4
  29. data/lib/tap/generator/generators/root/templates/{tapfile → Rapfile} +6 -6
  30. data/lib/tap/generator/generators/root/templates/gemspec +0 -1
  31. data/lib/tap/generator/generators/task/task_generator.rb +3 -3
  32. data/lib/tap/generator/generators/task/templates/test.erb +1 -1
  33. data/lib/tap/generator/manifest.rb +7 -1
  34. data/lib/tap/generator/preview.rb +76 -0
  35. data/lib/tap/root.rb +222 -156
  36. data/lib/tap/spec.rb +41 -0
  37. data/lib/tap/support/aggregator.rb +25 -28
  38. data/lib/tap/support/audit.rb +278 -357
  39. data/lib/tap/support/constant.rb +2 -1
  40. data/lib/tap/support/constant_manifest.rb +28 -25
  41. data/lib/tap/support/dependency.rb +1 -1
  42. data/lib/tap/support/executable.rb +52 -183
  43. data/lib/tap/support/executable_queue.rb +50 -20
  44. data/lib/tap/support/gems.rb +1 -1
  45. data/lib/tap/support/intern.rb +0 -6
  46. data/lib/tap/support/join.rb +49 -83
  47. data/lib/tap/support/joins.rb +0 -3
  48. data/lib/tap/support/joins/switch.rb +13 -11
  49. data/lib/tap/support/joins/sync_merge.rb +25 -50
  50. data/lib/tap/support/manifest.rb +1 -0
  51. data/lib/tap/support/node.rb +140 -20
  52. data/lib/tap/support/parser.rb +56 -42
  53. data/lib/tap/support/schema.rb +183 -157
  54. data/lib/tap/support/templater.rb +9 -1
  55. data/lib/tap/support/versions.rb +39 -0
  56. data/lib/tap/task.rb +150 -177
  57. data/lib/tap/tasks/dump.rb +4 -4
  58. data/lib/tap/tasks/load.rb +29 -29
  59. data/lib/tap/test.rb +66 -53
  60. data/lib/tap/test/env_vars.rb +3 -3
  61. data/lib/tap/test/extensions.rb +11 -17
  62. data/lib/tap/test/file_test.rb +74 -132
  63. data/lib/tap/test/file_test_class.rb +4 -1
  64. data/lib/tap/test/regexp_escape.rb +2 -2
  65. data/lib/tap/test/script_test.rb +2 -2
  66. data/lib/tap/test/subset_test.rb +6 -6
  67. data/lib/tap/test/tap_test.rb +28 -154
  68. metadata +30 -51
  69. data/bin/rap +0 -118
  70. data/cgi/run.rb +0 -97
  71. data/lib/tap/declarations.rb +0 -229
  72. data/lib/tap/generator/generators/config/templates/doc.erb +0 -12
  73. data/lib/tap/generator/generators/config/templates/nodoc.erb +0 -8
  74. data/lib/tap/generator/generators/file_task/file_task_generator.rb +0 -27
  75. data/lib/tap/generator/generators/file_task/templates/file.txt +0 -11
  76. data/lib/tap/generator/generators/file_task/templates/result.yml +0 -6
  77. data/lib/tap/generator/generators/file_task/templates/task.erb +0 -33
  78. data/lib/tap/generator/generators/file_task/templates/test.erb +0 -29
  79. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +0 -5
  80. data/lib/tap/patches/optparse/summarize.rb +0 -62
  81. data/lib/tap/support/assignments.rb +0 -173
  82. data/lib/tap/support/class_configuration.rb +0 -182
  83. data/lib/tap/support/combinator.rb +0 -125
  84. data/lib/tap/support/configurable.rb +0 -113
  85. data/lib/tap/support/configurable_class.rb +0 -271
  86. data/lib/tap/support/configuration.rb +0 -170
  87. data/lib/tap/support/gems/rake.rb +0 -111
  88. data/lib/tap/support/instance_configuration.rb +0 -173
  89. data/lib/tap/support/joins/fork.rb +0 -19
  90. data/lib/tap/support/joins/merge.rb +0 -22
  91. data/lib/tap/support/joins/sequence.rb +0 -21
  92. data/lib/tap/support/lazy_attributes.rb +0 -45
  93. data/lib/tap/support/lazydoc.rb +0 -386
  94. data/lib/tap/support/lazydoc/comment.rb +0 -503
  95. data/lib/tap/support/lazydoc/config.rb +0 -17
  96. data/lib/tap/support/lazydoc/definition.rb +0 -36
  97. data/lib/tap/support/lazydoc/document.rb +0 -152
  98. data/lib/tap/support/lazydoc/method.rb +0 -24
  99. data/lib/tap/support/tdoc.rb +0 -409
  100. data/lib/tap/support/tdoc/tdoc_html_generator.rb +0 -38
  101. data/lib/tap/support/tdoc/tdoc_html_template.rb +0 -42
  102. data/lib/tap/support/validation.rb +0 -479
  103. data/lib/tap/tasks/rake.rb +0 -57
@@ -5,23 +5,33 @@ module Tap::Generator::Generators
5
5
  # Generates a new generator.
6
6
  class GeneratorGenerator < Tap::Generator::Base
7
7
 
8
+ config :test, true, &c.switch # specifies creation of a test file
9
+
8
10
  def manifest(m, const_name)
9
- const = Constant.new(const_name.camelize)
10
- dir= File.join('lib', const.path)
11
+ const = Tap::Support::Constant.new(const_name.camelize)
12
+ dir = app.filepath('lib', const.path)
11
13
 
12
14
  # make the directory
13
- m.directory app.filepath(dir)
15
+ m.directory dir
14
16
 
15
17
  # make the generator
16
- m.template app.filepath(dir, const.basename + '_generator.rb'), "task.erb", :const => const
18
+ m.template app.filepath(dir, "#{const.basename}_generator.rb"), "task.erb", :const => const
17
19
 
18
20
  # make the templates directory
19
21
  m.directory app.filepath(dir, 'templates')
22
+
23
+ # make a template file
24
+ # (note it's easier to do this as a file since erb is
25
+ # added, and would have to be escaped in a template)
20
26
  m.file app.filepath(dir, 'templates', 'template_file.erb') do |file|
21
- file.puts "# A sample template file."
22
- file.puts "key: <%= key %>"
27
+ file << "# A sample template file.\nkey: <%= key %>\n"
23
28
  end
24
29
 
30
+ if test
31
+ test_path = app.filepath('test', "#{const.path}_generator_test.rb")
32
+ m.directory File.dirname(test_path)
33
+ m.template test_path, "test.erb", :const => const
34
+ end
25
35
  end
26
36
  end
27
37
  end
@@ -2,7 +2,7 @@
2
2
  # <replace with command line description>
3
3
 
4
4
  # <%= const.const_name %> Documentation
5
- class <%= const.const_name %> < Tap::Generator::Base
5
+ class <%= const.const_name %>Generator < Tap::Generator::Base
6
6
 
7
7
  config :key, 'value' # a sample config
8
8
 
@@ -21,7 +21,7 @@ class <%= const.const_name %> < Tap::Generator::Base
21
21
  # template a file in the templates directory using ERB.
22
22
  # The (key, value) pairs will be available in the
23
23
  # template as local variables.
24
- m.template "<%= const.const_name.underscore %>_file.txt", "template_file.erb", :key => 'value'
24
+ m.template "<%= const.const_name.underscore %>_file.txt", "template_file.erb", config.to_hash
25
25
 
26
26
  end
27
27
  end <% module_nest(const.nesting, ' ') { target } end %>
@@ -0,0 +1,26 @@
1
+ require File.join(File.dirname(__FILE__), '<%= '../' * const.nesting_depth %>tap_test_helper.rb')
2
+ require '<%= const.path %>'
3
+ require 'tap/generator/preview.rb'
4
+
5
+ class <%= const.name %>Test < Test::Unit::TestCase
6
+
7
+ # Preview fakes out a generator for testing
8
+ Preview = Tap::Generator::Preview
9
+
10
+ acts_as_tap_test
11
+
12
+ def test_<%= const.basename %>
13
+ g = <%= const.name %>.new.extend Preview
14
+
15
+ # check the files and directories
16
+ assert_equal %w{
17
+ <%= const.const_name.underscore %>_file.txt
18
+ }, g.process
19
+
20
+ # check the content as necessary
21
+ assert_equal %q{
22
+ # A sample template file.
23
+ key: value
24
+ }, "\n" + g.builds['<%= const.const_name.underscore %>_file.txt']
25
+ end
26
+ end
@@ -1,4 +1,4 @@
1
- require 'tap/root'
1
+ require 'tap/generator/base'
2
2
 
3
3
  module Tap::Generator::Generators
4
4
 
@@ -21,7 +21,7 @@ module Tap::Generator::Generators
21
21
  class RootGenerator < Tap::Generator::Base
22
22
 
23
23
  config :config_file, true, &c.switch # create a tap.yml file
24
- config :tapfile, false, &c.switch # create a tapfile
24
+ config :rapfile, false, &c.switch # create a rapfile
25
25
 
26
26
  # ::args ROOT, PROJECT_NAME=basename(ROOT)
27
27
  def manifest(m, root, project_name=nil)
@@ -30,32 +30,43 @@ module Tap::Generator::Generators
30
30
 
31
31
  m.directory r.root
32
32
  m.directory r['lib']
33
+ m.directory r['test']
33
34
 
34
35
  template_files do |source, target|
35
36
  case
36
37
  when File.directory?(source)
37
38
  m.directory r[target]
38
39
  next
39
- when target == 'gemspec'
40
- m.template r[project_name + '.gemspec'], source, :project_name => project_name, :tapfile => tapfile, :config_file => config_file
40
+ when source =~ /gemspec$/
41
+ m.template r[project_name + '.gemspec'], source, :project_name => project_name, :config_file => config_file
41
42
  next
42
- when target =~ /tapfile/i
43
- next unless tapfile
43
+ when source =~ /Rapfile$/
44
+ next unless rapfile
44
45
  end
45
46
 
46
47
  m.template r[target], source, :project_name => project_name
47
48
  end
48
49
 
49
50
  m.file(r['tap.yml']) do |file|
50
- Tap::App.configurations.inspect(:doc, file) do |templater|
51
- next unless templater.receiver == Tap::Root
51
+ Configurable::Utils.dump(Tap::App.configurations, file) do |key, delegate|
52
+ default = delegate.default
52
53
 
53
- templater.configurations.each do |(key, config)|
54
- config.default = nil if key.to_s == 'root'
55
- end
56
- end
57
- Tap::Env.configurations.inspect(:doc, file)
54
+ # get the description
55
+ desc = delegate.attributes[:desc]
56
+ doc = desc.to_s
57
+ doc = desc.comment if doc.empty?
58
+
59
+ # wrap as lines
60
+ lines = Lazydoc::Utils.wrap(doc, 50).collect {|line| "# #{line}"}
61
+ lines << "" unless lines.empty?
62
+
63
+ # setup formatting
64
+ leader = key == 'root' || default == nil ? '# ' : ''
65
+ config = {key => default}.to_yaml[5..-1]
66
+ "#{lines.join("\n")}#{leader}#{config.strip}\n\n"
67
+ end
58
68
  end if config_file
59
69
  end
70
+
60
71
  end
61
72
  end
@@ -62,12 +62,12 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
62
62
  rdoc.rdoc_files.include( spec.extra_rdoc_files )
63
63
  rdoc.rdoc_files.include( spec.files.select {|file| file =~ /^lib.*\.rb$/} )
64
64
 
65
- # Using Tdoc to template your Rdoc will result in configurations being
65
+ # Using CDoc to template your RDoc will result in configurations being
66
66
  # listed with documentation in a subsection following attributes. Not
67
67
  # necessary, but nice.
68
- require 'tap/support/tdoc'
69
- rdoc.template = 'tap/support/tdoc/tdoc_html_template'
70
- rdoc.options << '--fmt' << 'tdoc'
68
+ require 'cdoc'
69
+ rdoc.template = 'cdoc/cdoc_html_template'
70
+ rdoc.options << '--fmt' << 'cdoc'
71
71
  end
72
72
 
73
73
  #
@@ -1,11 +1,11 @@
1
1
  require 'tap/declarations'
2
2
 
3
3
  module <%= project_name.camelize %>
4
- extend Tap::Declarations
5
-
6
- # ::desc your basic goodnight moon task
7
- # Says goodnight with a configurable message.
8
- task(:goodnight, :obj, :message => 'goodnight') do |task, args|
9
- puts "#{task.message} #{args.obj}"
4
+ extend Rap::Declarations
5
+
6
+ # ::desc your basic goodnight moon task
7
+ # Says goodnight with a configurable message.
8
+ task(:goodnight, :obj, :message => 'goodnight') do |task, args|
9
+ puts "#{task.message} #{args.obj}"
10
10
  end
11
11
  end
@@ -22,6 +22,5 @@ Gem::Specification.new do |s|
22
22
  s.files = %W{
23
23
  <%= config_file ? " tap.yml\n" : '' %>
24
24
  test/tap_test_helper.rb
25
- test/tap_test_suite.rb
26
25
  }
27
26
  end
@@ -5,15 +5,15 @@ module Tap::Generator::Generators
5
5
  # Generates a new Tap::Task and an associated test file.
6
6
  class TaskGenerator < Tap::Generator::Base
7
7
 
8
- config :test, true, &c.switch # Generates the task without test files.
8
+ config :test, true, &c.switch # specifies creation of a test file
9
9
 
10
10
  def manifest(m, const_name)
11
- const = Constant.new(const_name.camelize)
11
+ const = Tap::Support::Constant.new(const_name.camelize)
12
12
 
13
13
  task_path = app.filepath('lib', "#{const.path}.rb")
14
14
  m.directory File.dirname(task_path)
15
15
  m.template task_path, "task.erb", :const => const
16
-
16
+
17
17
  if test
18
18
  test_path = app.filepath('test', "#{const.path}_test.rb")
19
19
  m.directory File.dirname(test_path)
@@ -14,6 +14,6 @@ class <%= const.name %>Test < Test::Unit::TestCase
14
14
  # a more complex test
15
15
  task.execute("moon")
16
16
  assert_equal ["goodnight moon"], app.results(task)
17
- assert_audit_equal ExpAudit[[nil, "moon"], [task, "goodnight moon"]], app._results(task)[0]
17
+ assert_audit_equal [[nil, "moon"], [task, "goodnight moon"]], app._results(task)[0]
18
18
  end
19
19
  end
@@ -1,11 +1,17 @@
1
1
  module Tap
2
2
  module Generator
3
+
4
+ # Manifest records methods called upon it using method_missing. These
5
+ # actions are replayed on a generator in order (for generate) or in
6
+ # reverse order (for destroy).
3
7
  class Manifest
8
+
9
+ # Makes a new Manifest. Method calls on self are recorded to actions.
4
10
  def initialize(actions)
5
11
  @actions = actions
6
12
  end
7
13
 
8
- # Record an action.
14
+ # Records an action.
9
15
  def method_missing(action, *args, &block)
10
16
  @actions << [action, args, block]
11
17
  end
@@ -0,0 +1,76 @@
1
+ require 'stringio'
2
+
3
+ module Tap
4
+ module Generator
5
+
6
+ # Preview is a testing module designed so that process will return an array
7
+ # of relative filepaths for the created files/directories (which are easy
8
+ # to specify in a test). Preview also collects the content of created files
9
+ # to be tested as needed.
10
+ #
11
+ # class Sample < Tap::Generator::Base
12
+ # def manifest(m)
13
+ # dir = app.filepath(:root, 'dir')
14
+ #
15
+ # m.directory dir
16
+ # m.file(File.join(dir, 'file.txt')) {|io| io << "content"}
17
+ # end
18
+ # end
19
+ #
20
+ # These assertions will pass:
21
+ #
22
+ # s = Sample.new.extend Preview
23
+ # assert_equal %w{
24
+ # dir
25
+ # dir/file.txt
26
+ # }, s.process
27
+ #
28
+ # assert_equal "content", s.preview['dir/file.txt']
29
+ #
30
+ # Note that relative filepaths are determined from app.root for the
31
+ # instance; in tests like the one above, it may be prudent to reset
32
+ # the Tap::App.instance like so:
33
+ #
34
+ # def setup
35
+ # Tap::App.instance = Tap::App.new
36
+ # end
37
+ #
38
+ module Preview
39
+
40
+ # A hash of (relative_path, content) pairs representing
41
+ # content built to files.
42
+ attr_accessor :preview
43
+
44
+ def self.extended(base) # :nodoc:
45
+ base.instance_variable_set(:@preview, {})
46
+ end
47
+
48
+ # Returns the path of path, relative to app.root. If path
49
+ # is app.root, '.' will be returned.
50
+ def relative_path(path)
51
+ path = app.relative_filepath(:root, path) || path
52
+ path.empty? ? "." : path
53
+ end
54
+
55
+ # Returns the relative path of the target.
56
+ def directory(target, options={})
57
+ relative_path(target)
58
+ end
59
+
60
+ # Returns the relative path of the target. If a block is given,
61
+ # the block will be called with a StringIO and the results stored
62
+ # in builds.
63
+ def file(target, options={})
64
+ target = relative_path(target)
65
+
66
+ if block_given?
67
+ io = StringIO.new
68
+ yield(io)
69
+ preview[target] = io.string
70
+ end
71
+
72
+ target
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,59 +1,58 @@
1
+ require 'configurable'
1
2
  require 'tap/support/versions'
2
- require 'tap/support/configurable'
3
3
  autoload(:FileUtils, 'fileutils')
4
4
 
5
5
  module Tap
6
6
 
7
- # Root allows you to define a root directory and alias subdirectories, so that
8
- # you can conceptualize what filepaths you need without predefining the full
9
- # filepaths. Root also simplifies operations on filepaths.
7
+ # Root allows you to define a root directory and alias relative paths, so
8
+ # that you can conceptualize what filepaths you need without predefining the
9
+ # full filepaths. Root also simplifies operations on filepaths.
10
10
  #
11
- # # define a root directory with aliased subdirectories
12
- # r = Root.new '/root_dir', :input => 'in', :output => 'out'
11
+ # # define a root directory with aliased relative paths
12
+ # r = Root.new '/root_dir', :input => 'in', :output => 'out'
13
13
  #
14
- # # work with directories
15
- # r[:input] # => '/root_dir/in'
16
- # r[:output] # => '/root_dir/out'
17
- # r['implicit'] # => '/root_dir/implicit'
14
+ # # work with aliases
15
+ # r[:input] # => '/root_dir/in'
16
+ # r[:output] # => '/root_dir/out'
17
+ # r['implicit'] # => '/root_dir/implicit'
18
18
  #
19
- # # expanded paths are returned unchanged
20
- # r[File.expand_path('expanded')] # => File.expand_path('expanded')
19
+ # # expanded paths are returned unchanged
20
+ # r[File.expand_path('expanded')] # => File.expand_path('expanded')
21
21
  #
22
- # # work with filepaths
23
- # fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
24
- # r.relative_filepath(:input, fp) # => 'path/to/file.txt'
25
- # r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
22
+ # # work with filepaths
23
+ # fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
24
+ # r.relative_filepath(:input, fp) # => 'path/to/file.txt'
25
+ # r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
26
26
  #
27
- # # version filepaths
28
- # r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
29
- # r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
30
- # r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
27
+ # # version filepaths
28
+ # r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
29
+ # r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
30
+ # r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
31
31
  #
32
- # # absolute paths can also be aliased
33
- # r[:abs, true] = "/absolute/path"
34
- # r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
32
+ # # absolute paths can also be aliased
33
+ # r[:abs, true] = "/absolute/path"
34
+ # r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
35
35
  #
36
- # By default, Roots are initialized to the present working directory (Dir.pwd).
37
- # As in the 'implicit' example, Root infers a path relative to the root directory
38
- # whenever it needs to resolve an alias that is not explicitly set. The only
39
- # exceptions to this are fully expanded paths. These are returned unchanged.
36
+ # By default, Roots are initialized to the present working directory
37
+ # (Dir.pwd).
40
38
  #
39
+ #--
41
40
  # === Implementation Notes
42
41
  #
43
- # Internally Root stores expanded paths all aliased paths in the 'paths' hash.
44
- # Expanding paths ensures they remain constant even when the present working
42
+ # Internally Root expands and stores all aliased paths in the 'paths' hash.
43
+ # Expanding paths ensures they remain constant even when the present working
45
44
  # directory (Dir.pwd) changes.
46
45
  #
47
- # Root keeps a separate 'directories' hash mapping aliases to their subdirectory paths.
48
- # This hash allow reassignment if and when the root directory changes. By contrast,
49
- # there is no separate data structure storing the absolute paths. An absolute path
50
- # thus has an alias in 'paths' but not 'directories', whereas subdirectory paths
51
- # have aliases in both.
46
+ # Root keeps a separate 'relative_paths' hash mapping aliases to their
47
+ # relative paths. This hash allow reassignment if and when the root directory
48
+ # changes. By contrast, there is no separate data structure storing the
49
+ # absolute paths. An absolute path thus has an alias in 'paths' but not
50
+ # 'relative_paths', whereas relative paths have aliases in both.
52
51
  #
53
52
  # These features may be important to note when subclassing Root:
54
53
  # - root and all filepaths in 'paths' are expanded
55
- # - subdirectory paths are stored in 'directories'
56
- # - absolute paths are present in 'paths' but not in 'directories'
54
+ # - relative paths are stored in 'relative_paths'
55
+ # - absolute paths are present in 'paths' but not in 'relative_paths'
57
56
  #
58
57
  class Root
59
58
  # Regexp to match a windows-style root filepath.
@@ -77,12 +76,12 @@ module Tap
77
76
  # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
78
77
  # as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
79
78
  # empty string is returned
80
- expanded_path[( expanded_dir.chomp("/").length + 1)..-1] || ""
79
+ expanded_path[(expanded_dir.chomp("/").length + 1)..-1] || ""
81
80
  end
82
81
 
83
- # Generates a target filepath translated from the source_dir to
84
- # the target_dir. Raises an error if the filepath is not relative
85
- # to the source_dir.
82
+ # Generates a target filepath translated from the source_dir to the
83
+ # target_dir. Raises an error if the filepath is not relative to the
84
+ # source_dir.
86
85
  #
87
86
  # Root.translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
88
87
  #
@@ -92,6 +91,16 @@ module Tap
92
91
  end
93
92
  File.join(target_dir, relative_path)
94
93
  end
94
+
95
+ # Returns the path, exchanging the extension with extname. Extname may
96
+ # optionally omit the leading period.
97
+ #
98
+ # Root.exchange('path/to/file.txt', '.html') # => 'path/to/file.html'
99
+ # Root.exchange('path/to/file.txt', 'rb') # => 'path/to/file.rb'
100
+ #
101
+ def exchange(path, extname)
102
+ "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
103
+ end
95
104
 
96
105
  # Lists all unique paths matching the input glob patterns.
97
106
  def glob(*patterns)
@@ -100,8 +109,8 @@ module Tap
100
109
  end.flatten.uniq
101
110
  end
102
111
 
103
- # Lists all unique versions of path matching the glob version patterns.
104
- # If no patterns are specified, then all versions of path will be returned.
112
+ # Lists all unique versions of path matching the glob version patterns. If
113
+ # no patterns are specified, then all versions of path will be returned.
105
114
  def vglob(path, *vpatterns)
106
115
  vpatterns << "*" if vpatterns.empty?
107
116
  vpatterns.collect do |vpattern|
@@ -113,8 +122,8 @@ module Tap
113
122
  end.flatten.uniq
114
123
  end
115
124
 
116
- # Path suffix glob. Globs along the base paths for
117
- # paths that match the specified suffix pattern.
125
+ # Path suffix glob. Globs along the base paths for paths that match the
126
+ # specified suffix pattern.
118
127
  def sglob(suffix_pattern, *base_paths)
119
128
  base_paths.collect do |base|
120
129
  base = File.expand_path(base)
@@ -122,9 +131,9 @@ module Tap
122
131
  end.flatten.uniq
123
132
  end
124
133
 
125
- # Like Dir.chdir but makes the directory, if necessary, when
126
- # mkdir is specified. chdir raises an error for non-existant
127
- # directories, as well as non-directory inputs.
134
+ # Like Dir.chdir but makes the directory, if necessary, when mkdir is
135
+ # specified. chdir raises an error for non-existant directories, as well
136
+ # as non-directory inputs.
128
137
  def chdir(dir, mkdir=false, &block)
129
138
  dir = File.expand_path(dir)
130
139
 
@@ -139,8 +148,20 @@ module Tap
139
148
  Dir.chdir(dir, &block)
140
149
  end
141
150
 
142
- # The path root type indicating windows, *nix, or some unknown
143
- # style of filepaths (:win, :nix, :unknown).
151
+ # Prepares the input path by making the parent directory for path. If a
152
+ # block is given, a file is created at path and passed to it; in this
153
+ # way files with non-existant parent directories are readily made.
154
+ #
155
+ # Returns path.
156
+ def prepare(path, &block)
157
+ dirname = File.dirname(path)
158
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
159
+ File.open(path, "w", &block) if block_given?
160
+ path
161
+ end
162
+
163
+ # The path root type indicating windows, *nix, or some unknown style of
164
+ # filepaths (:win, :nix, :unknown).
144
165
  def path_root_type
145
166
  @path_root_type ||= case
146
167
  when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
@@ -149,25 +170,24 @@ module Tap
149
170
  end
150
171
  end
151
172
 
152
- # Returns true if the input path appears to be an expanded path,
153
- # based on Root.path_root_type.
173
+ # Returns true if the input path appears to be an expanded path, based on
174
+ # Root.path_root_type.
154
175
  #
155
- # If root_type == :win returns true if the path matches
156
- # WIN_ROOT_PATTERN.
176
+ # If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
157
177
  #
158
- # Root.expanded_path?('C:/path') # => true
159
- # Root.expanded_path?('c:/path') # => true
160
- # Root.expanded_path?('D:/path') # => true
161
- # Root.expanded_path?('path') # => false
178
+ # Root.expanded?('C:/path') # => true
179
+ # Root.expanded?('c:/path') # => true
180
+ # Root.expanded?('D:/path') # => true
181
+ # Root.expanded?('path') # => false
162
182
  #
163
- # If root_type == :nix, then expanded? returns true if
164
- # the path begins with '/'.
183
+ # If root_type == :nix, then expanded? returns true if the path begins
184
+ # with '/'.
165
185
  #
166
- # Root.expanded_path?('/path') # => true
167
- # Root.expanded_path?('path') # => false
186
+ # Root.expanded?('/path') # => true
187
+ # Root.expanded?('path') # => false
168
188
  #
169
- # Otherwise expanded_path? always returns nil.
170
- def expanded_path?(path, root_type=path_root_type)
189
+ # Otherwise expanded? always returns nil.
190
+ def expanded?(path, root_type=path_root_type)
171
191
  case root_type
172
192
  when :win
173
193
  path =~ WIN_ROOT_PATTERN ? true : false
@@ -178,6 +198,17 @@ module Tap
178
198
  end
179
199
  end
180
200
 
201
+ # Trivial indicates when a path does not have content to load. Returns
202
+ # true if the file at path is empty, non-existant, a directory, or nil.
203
+ def trivial?(path)
204
+ path == nil || !File.file?(path) || File.size(path) == 0
205
+ end
206
+
207
+ # Empty returns true when dir is an existing directory that has no files.
208
+ def empty?(dir)
209
+ File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
210
+ end
211
+
181
212
  # Minimizes a set of paths to the set of shortest basepaths that unqiuely
182
213
  # identify the paths. The path extension and versions are removed from
183
214
  # the basepath if possible. For example:
@@ -267,8 +298,9 @@ module Tap
267
298
  end
268
299
  end
269
300
 
270
- # Returns true if the mini_path matches path. Matching logic
271
- # reverses that of minimize:
301
+ # Returns true if the mini_path matches path. Matching logic reverses
302
+ # that of minimize:
303
+ #
272
304
  # * a match occurs when path ends with mini_path
273
305
  # * if mini_path doesn't specify an extension, then mini_path
274
306
  # must only match path up to the path extension
@@ -324,11 +356,12 @@ module Tap
324
356
  # windows '\' Root.split('C:\path\to\file') # => ["C:", "path", "to", "file"]
325
357
  # *nix '/' Root.split('/path/to/file') # => ["", "path", "to", "file"]
326
358
  #
327
- # The path is always expanded relative to the expand_dir; so '.' and '..' are
328
- # resolved. However, unless expand_path == true, only the segments relative
329
- # to the expand_dir are returned.
359
+ # The path is always expanded relative to the expand_dir; so '.' and
360
+ # '..' are resolved. However, unless expand_path == true, only the
361
+ # segments relative to the expand_dir are returned.
330
362
  #
331
- # On windows (note that expanding paths allows the use of slashes or backslashes):
363
+ # On windows (note that expanding paths allows the use of slashes or
364
+ # backslashes):
332
365
  #
333
366
  # Dir.pwd # => 'C:/'
334
367
  # Root.split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
@@ -365,6 +398,7 @@ module Tap
365
398
  #
366
399
  # "./path"
367
400
  # "//path"
401
+ #
368
402
  def min_join(dir, path) # :nodoc:
369
403
  case dir
370
404
  when "." then path
@@ -407,89 +441,99 @@ module Tap
407
441
  end
408
442
 
409
443
  end
410
-
444
+
445
+ include Configurable
411
446
  include Support::Versions
412
- include Support::Configurable
413
-
447
+
414
448
  # The root directory.
415
449
  config_attr(:root, '.', :writer => false)
416
450
 
417
- # A hash of (alias, relative path) pairs for aliased subdirectories.
418
- config_attr(:directories, {}, :writer => false)
451
+ # A hash of (alias, relative path) pairs for aliased paths relative
452
+ # to root.
453
+ config_attr(:relative_paths, {}, :writer => false)
419
454
 
420
455
  # A hash of (alias, relative path) pairs for aliased absolute paths.
421
456
  config_attr(:absolute_paths, {}, :reader => false, :writer => false)
422
457
 
423
- # A hash of (alias, expanded path) pairs for aliased subdirectories and absolute paths.
458
+ # A hash of (alias, expanded path) pairs for expanded relative and
459
+ # absolute paths.
424
460
  attr_reader :paths
425
461
 
426
462
  # The filesystem root, inferred from self.root
427
463
  # (ex '/' on *nix or something like 'C:/' on Windows).
428
464
  attr_reader :path_root
429
465
 
430
- # Creates a new Root with the given root directory, aliased directories
466
+ # Creates a new Root with the given root directory, aliased relative paths
431
467
  # and absolute paths. By default root is the present working directory
432
- # and no aliased directories or absolute paths are specified.
433
- def initialize(root=Dir.pwd, directories={}, absolute_paths={})
434
- assign_paths(root, directories, absolute_paths)
435
- @config = self.class.configurations.instance_config(self)
468
+ # and no aliased relative or absolute paths are specified.
469
+ def initialize(root=Dir.pwd, relative_paths={}, absolute_paths={})
470
+ assign_paths(root, relative_paths, absolute_paths)
471
+ @config = DelegateHash.new(self.class.configurations, {}, self)
436
472
  end
437
473
 
438
474
  # Sets the root directory. All paths are reassigned accordingly.
439
475
  def root=(path)
440
- assign_paths(path, directories, absolute_paths)
476
+ assign_paths(path, relative_paths, absolute_paths)
441
477
  end
442
478
 
443
- # Sets the directories to those provided. 'root' and :root are reserved
444
- # and cannot be set using this method (use root= instead).
479
+ # Sets the relative_paths to those provided. 'root' and :root are reserved
480
+ # aliases and cannot be set using this method (use root= instead).
445
481
  #
446
- # r['alt'] # => File.join(r.root, 'alt')
447
- # r.directories = {'alt' => 'dir'}
448
- # r['alt'] # => File.join(r.root, 'dir')
449
- def directories=(dirs)
450
- assign_paths(root, dirs, absolute_paths)
482
+ # r = Tap::Root.new
483
+ # r['alt'] # => File.join(r.root, 'alt')
484
+ # r.relative_paths = {'alt' => 'dir'}
485
+ # r['alt'] # => File.join(r.root, 'dir')
486
+ #
487
+ def relative_paths=(paths)
488
+ paths = Validation::HASH[paths]
489
+ assign_paths(root, paths, absolute_paths)
451
490
  end
452
491
 
453
492
  # Sets the absolute paths to those provided. 'root' and :root are reserved
454
- # directory keys and cannot be set using this method (use root= instead).
493
+ # aliases and cannot be set using this method (use root= instead).
494
+ #
495
+ # r = Tap::Root.new
496
+ # r['abs'] # => File.join(r.root, 'abs')
497
+ # r.absolute_paths = {'abs' => '/path/to/dir'}
498
+ # r['abs'] # => '/path/to/dir'
455
499
  #
456
- # r['abs'] # => File.join(r.root, 'abs')
457
- # r.absolute_paths = {'abs' => '/path/to/dir'}
458
- # r['abs'] # => '/path/to/dir'
459
500
  def absolute_paths=(paths)
460
- assign_paths(root, directories, paths)
501
+ paths = Validation::HASH[paths]
502
+ assign_paths(root, relative_paths, paths)
461
503
  end
462
504
 
463
505
  # Returns the absolute paths registered with self.
464
506
  def absolute_paths
465
507
  abs_paths = {}
466
- paths.each do |da, path|
467
- abs_paths[da] = path unless directories.include?(da) || da.to_s == 'root'
508
+ paths.each do |als, path|
509
+ unless relative_paths.include?(als) || als.to_s == 'root'
510
+ abs_paths[als] = path
511
+ end
468
512
  end
469
513
  abs_paths
470
514
  end
471
515
 
472
- # Sets an alias for the subdirectory relative to the root directory.
473
- # The aliases 'root' and :root cannot be set with this method
474
- # (use root= instead). Absolute filepaths can be set using the
475
- # second syntax.
516
+ # Sets an alias for the path relative to the root directory. The aliases
517
+ # 'root' and :root cannot be set with this method (use root= instead).
518
+ # Absolute filepaths can be set using the second syntax.
476
519
  #
477
- # r = Root.new '/root_dir'
478
- # r[:dir] = 'path/to/dir'
479
- # r[:dir] # => '/root_dir/path/to/dir'
520
+ # r = Root.new '/root_dir'
521
+ # r[:dir] = 'path/to/dir'
522
+ # r[:dir] # => '/root_dir/path/to/dir'
480
523
  #
481
- # r[:abs, true] = '/abs/path/to/dir'
482
- # r[:abs] # => '/abs/path/to/dir'
524
+ # r[:abs, true] = '/abs/path/to/dir'
525
+ # r[:abs] # => '/abs/path/to/dir'
483
526
  #
484
527
  #--
485
- # Implementation Notes:
528
+ # Implementation Note:
529
+ #
486
530
  # The syntax for setting an absolute filepath requires an odd use []=.
487
531
  # In fact the method recieves the arguments (:dir, true, '/abs/path/to/dir')
488
532
  # rather than (:dir, '/abs/path/to/dir', true), meaning that internally path
489
533
  # and absolute are switched when setting an absolute filepath.
490
- #++
491
- def []=(dir, path, absolute=false)
492
- raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.to_s == 'root'
534
+ #
535
+ def []=(als, path, absolute=false)
536
+ raise ArgumentError, "the alias #{als.inspect} is reserved" if als.to_s == 'root'
493
537
 
494
538
  # switch the paths if absolute was provided
495
539
  unless absolute == false
@@ -500,84 +544,106 @@ module Tap
500
544
 
501
545
  case
502
546
  when path.nil?
503
- @directories.delete(dir)
504
- @paths.delete(dir)
547
+ @relative_paths.delete(als)
548
+ @paths.delete(als)
505
549
  when absolute
506
- @directories.delete(dir)
507
- @paths[dir] = File.expand_path(path)
550
+ @relative_paths.delete(als)
551
+ @paths[als] = File.expand_path(path)
508
552
  else
509
- @directories[dir] = path
510
- @paths[dir] = File.expand_path(File.join(root, path))
553
+ @relative_paths[als] = path
554
+ @paths[als] = File.expand_path(File.join(root, path))
511
555
  end
512
556
  end
513
557
 
514
- # Returns the expanded path for the specified alias. If the alias
515
- # has not been set, then the path is inferred to be 'root/dir' unless
516
- # the path is relative to path_root. These paths are returned
517
- # directly.
558
+ # Returns the expanded path for the specified alias. If the alias has not
559
+ # been set, then the path is inferred to be 'root/als'. Expanded paths
560
+ # are returned directly.
518
561
  #
519
- # r = Root.new '/root_dir', :dir => 'path/to/dir'
520
- # r[:dir] # => '/root_dir/path/to/dir'
562
+ # r = Root.new '/root_dir', :dir => 'path/to/dir'
563
+ # r[:dir] # => '/root_dir/path/to/dir'
521
564
  #
522
- # r.path_root # => '/'
523
- # r['relative/path'] # => '/root_dir/relative/path'
524
- # r['/expanded/path'] # => '/expanded/path'
565
+ # r.path_root # => '/'
566
+ # r['relative/path'] # => '/root_dir/relative/path'
567
+ # r['/expanded/path'] # => '/expanded/path'
525
568
  #
526
- def [](dir)
527
- path = self.paths[dir]
569
+ def [](als)
570
+ path = self.paths[als]
528
571
  return path unless path == nil
529
572
 
530
- dir = dir.to_s
531
- Root.expanded_path?(dir) ? dir : File.expand_path(File.join(root, dir))
573
+ als = als.to_s
574
+ Root.expanded?(als) ? als : File.expand_path(File.join(root, als))
532
575
  end
533
576
 
534
- # Constructs expanded filepaths relative to the path of the specified alias.
535
- def filepath(dir, *filename)
536
- # TODO - consider filename.compact so nils will not raise errors
537
- File.expand_path(File.join(self[dir], *filename))
577
+ # Resolves the specified alias, joins the paths together, and expands the
578
+ # resulting filepath.
579
+ def filepath(als, *paths)
580
+ File.expand_path(File.join(self[als], *paths))
538
581
  end
539
582
 
540
583
  # Retrieves the filepath relative to the path of the specified alias.
541
- def relative_filepath(dir, filepath)
542
- Root.relative_filepath(self[dir], filepath)
584
+ def relative_filepath(als, path)
585
+ Root.relative_filepath(self[als], path)
586
+ end
587
+
588
+ # Same as filepath but raises an error if the result is not a subpath of
589
+ # the aliased directory.
590
+ def subpath(als, *paths)
591
+ dir = self[als]
592
+ path = filepath(als, *paths)
593
+
594
+ if path.rindex(dir, 0) != 0
595
+ raise "not a subpath: #{path} (#{dir})"
596
+ end
597
+
598
+ path
543
599
  end
544
600
 
545
- # Generates a target filepath translated from the aliased source_dir to
546
- # the aliased target_dir. Raises an error if the filepath is not relative
547
- # to the aliased source_dir.
601
+ # Generates a filepath translated from the aliased source dir to the
602
+ # aliased target dir. Raises an error if the filepath is not relative
603
+ # to the source dir.
548
604
  #
549
- # fp = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
550
- # r.translate(fp, :in, :out) # => '/root_dir/out/path/to/file.txt'
551
- def translate(filepath, source_dir, target_dir)
552
- Root.translate(filepath, self[source_dir], self[target_dir])
605
+ # r = Tap::Root.new '/root_dir'
606
+ # path = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
607
+ # r.translate(path, :in, :out) # => '/root_dir/out/path/to/file.txt'
608
+ #
609
+ def translate(path, source_als, target_als)
610
+ Root.translate(path, self[source_als], self[target_als])
553
611
  end
554
612
 
555
- # Lists all files in the aliased dir matching the input patterns. Patterns
556
- # should be valid inputs for +Dir.glob+. If no patterns are specified, lists
557
- # all files/folders matching '**/*'.
558
- def glob(dir, *patterns)
613
+ # Lists all files along the aliased path matching the input patterns.
614
+ # Patterns should join with the aliased path make valid globs for
615
+ # Dir.glob. If no patterns are specified, glob returns all paths
616
+ # matching 'als/**/*'.
617
+ def glob(als, *patterns)
559
618
  patterns << "**/*" if patterns.empty?
560
- patterns.collect! {|pattern| filepath(dir, pattern)}
619
+ patterns.collect! {|pattern| filepath(als, pattern)}
561
620
  Root.glob(*patterns)
562
621
  end
563
622
 
564
- # Lists all versions of filename in the aliased dir matching the version patterns.
565
- # If no patterns are specified, then all versions of filename will be returned.
566
- def vglob(dir, filename, *vpatterns)
567
- Root.vglob(filepath(dir, filename), *vpatterns)
623
+ # Lists all versions of path in the aliased dir matching the version
624
+ # patterns. If no patterns are specified, then all versions of path
625
+ # will be returned.
626
+ def vglob(als, path, *vpatterns)
627
+ Root.vglob(filepath(als, path), *vpatterns)
628
+ end
629
+
630
+ # Changes pwd to the specified directory using Root.chdir.
631
+ def chdir(als, mkdir=false, &block)
632
+ Root.chdir(self[als], mkdir, &block)
568
633
  end
569
634
 
570
- # chdirs to the specified directory using Root.chdir.
571
- def chdir(dir, mkdir=false, &block)
572
- Root.chdir(self[dir], mkdir, &block)
635
+ # Constructs a path from the inputs (using filepath) and prepares it using
636
+ # Root.prepare. Returns the path.
637
+ def prepare(als, *paths, &block)
638
+ Root.prepare(filepath(als, *paths), &block)
573
639
  end
574
640
 
575
641
  private
576
642
 
577
- # reassigns all paths with the input root, directories, and absolute_paths
578
- def assign_paths(root, directories, absolute_paths)
643
+ # reassigns all paths with the input root, relative_paths, and absolute_paths
644
+ def assign_paths(root, relative_paths, absolute_paths)
579
645
  @root = File.expand_path(root)
580
- @directories = {}
646
+ @relative_paths = {}
581
647
  @paths = {'root' => @root, :root => @root}
582
648
 
583
649
  @path_root = File.dirname(@root)
@@ -585,7 +651,7 @@ module Tap
585
651
  @path_root = parent
586
652
  end
587
653
 
588
- directories.each_pair {|dir, path| self[dir] = path }
654
+ relative_paths.each_pair {|dir, path| self[dir] = path }
589
655
  absolute_paths.each_pair {|dir, path| self[dir, true] = path }
590
656
  end
591
657