tap 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +71 -0
  3. data/Rakefile +117 -0
  4. data/bin/tap +63 -0
  5. data/lib/tap.rb +15 -0
  6. data/lib/tap/app.rb +739 -0
  7. data/lib/tap/file_task.rb +354 -0
  8. data/lib/tap/generator.rb +29 -0
  9. data/lib/tap/generator/generators/config/USAGE +0 -0
  10. data/lib/tap/generator/generators/config/config_generator.rb +23 -0
  11. data/lib/tap/generator/generators/config/templates/config.erb +2 -0
  12. data/lib/tap/generator/generators/file_task/USAGE +0 -0
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
  16. data/lib/tap/generator/generators/root/USAGE +0 -0
  17. data/lib/tap/generator/generators/root/root_generator.rb +36 -0
  18. data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
  19. data/lib/tap/generator/generators/root/templates/app.yml +19 -0
  20. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
  21. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
  22. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  23. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
  24. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
  25. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
  26. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
  27. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
  28. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
  29. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
  30. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  31. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
  32. data/lib/tap/generator/generators/task/USAGE +0 -0
  33. data/lib/tap/generator/generators/task/task_generator.rb +21 -0
  34. data/lib/tap/generator/generators/task/templates/task.erb +21 -0
  35. data/lib/tap/generator/generators/task/templates/test.erb +29 -0
  36. data/lib/tap/generator/generators/workflow/USAGE +0 -0
  37. data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
  38. data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
  39. data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
  40. data/lib/tap/generator/options.rb +26 -0
  41. data/lib/tap/generator/usage.rb +26 -0
  42. data/lib/tap/root.rb +275 -0
  43. data/lib/tap/script/console.rb +7 -0
  44. data/lib/tap/script/destroy.rb +8 -0
  45. data/lib/tap/script/generate.rb +8 -0
  46. data/lib/tap/script/run.rb +111 -0
  47. data/lib/tap/script/server.rb +12 -0
  48. data/lib/tap/support/audit.rb +415 -0
  49. data/lib/tap/support/batch_queue.rb +165 -0
  50. data/lib/tap/support/combinator.rb +114 -0
  51. data/lib/tap/support/logger.rb +91 -0
  52. data/lib/tap/support/rap.rb +38 -0
  53. data/lib/tap/support/run_error.rb +20 -0
  54. data/lib/tap/support/template.rb +81 -0
  55. data/lib/tap/support/templater.rb +155 -0
  56. data/lib/tap/support/versions.rb +63 -0
  57. data/lib/tap/task.rb +448 -0
  58. data/lib/tap/test.rb +320 -0
  59. data/lib/tap/test/env_vars.rb +16 -0
  60. data/lib/tap/test/inference_methods.rb +298 -0
  61. data/lib/tap/test/subset_methods.rb +260 -0
  62. data/lib/tap/version.rb +3 -0
  63. data/lib/tap/workflow.rb +73 -0
  64. data/test/app/config/addition_template.yml +6 -0
  65. data/test/app/config/batch.yml +2 -0
  66. data/test/app/config/empty.yml +0 -0
  67. data/test/app/config/erb.yml +1 -0
  68. data/test/app/config/template.yml +6 -0
  69. data/test/app/config/version-0.1.yml +1 -0
  70. data/test/app/config/version.yml +1 -0
  71. data/test/app/lib/app_test_task.rb +2 -0
  72. data/test/app_class_test.rb +33 -0
  73. data/test/app_test.rb +1372 -0
  74. data/test/file_task/config/batch.yml +2 -0
  75. data/test/file_task/config/configured.yml +1 -0
  76. data/test/file_task/old_file_one.txt +0 -0
  77. data/test/file_task/old_file_two.txt +0 -0
  78. data/test/file_task_test.rb +1041 -0
  79. data/test/root/alt_lib/alt_module.rb +4 -0
  80. data/test/root/lib/absolute_alt_filepath.rb +2 -0
  81. data/test/root/lib/alternative_filepath.rb +2 -0
  82. data/test/root/lib/another_module.rb +2 -0
  83. data/test/root/lib/nested/some_module.rb +4 -0
  84. data/test/root/lib/no_module_included.rb +0 -0
  85. data/test/root/lib/some/module.rb +4 -0
  86. data/test/root/lib/some_class.rb +2 -0
  87. data/test/root/lib/some_module.rb +3 -0
  88. data/test/root/load_path/load_path_module.rb +2 -0
  89. data/test/root/load_path/skip_module.rb +2 -0
  90. data/test/root/mtime/older.txt +0 -0
  91. data/test/root/unload/full_path.rb +2 -0
  92. data/test/root/unload/loaded_by_nested.rb +2 -0
  93. data/test/root/unload/nested/nested_load.rb +6 -0
  94. data/test/root/unload/nested/nested_with_ext.rb +4 -0
  95. data/test/root/unload/nested/relative_path.rb +4 -0
  96. data/test/root/unload/older.rb +2 -0
  97. data/test/root/unload/unload_base.rb +9 -0
  98. data/test/root/versions/another.yml +0 -0
  99. data/test/root/versions/file-0.1.2.yml +0 -0
  100. data/test/root/versions/file-0.1.yml +0 -0
  101. data/test/root/versions/file.yml +0 -0
  102. data/test/root_test.rb +483 -0
  103. data/test/support/audit_test.rb +449 -0
  104. data/test/support/batch_queue_test.rb +320 -0
  105. data/test/support/combinator_test.rb +249 -0
  106. data/test/support/logger_test.rb +31 -0
  107. data/test/support/template_test.rb +122 -0
  108. data/test/support/templater/erb.txt +2 -0
  109. data/test/support/templater/erb.yml +2 -0
  110. data/test/support/templater/somefile.txt +2 -0
  111. data/test/support/templater_test.rb +192 -0
  112. data/test/support/versions_test.rb +71 -0
  113. data/test/tap_test_helper.rb +4 -0
  114. data/test/tap_test_suite.rb +4 -0
  115. data/test/task/config/batch.yml +2 -0
  116. data/test/task/config/batched.yml +2 -0
  117. data/test/task/config/configured.yml +1 -0
  118. data/test/task/config/example.yml +1 -0
  119. data/test/task/config/overriding.yml +2 -0
  120. data/test/task/config/task_with_config.yml +1 -0
  121. data/test/task/config/template.yml +4 -0
  122. data/test/task_class_test.rb +118 -0
  123. data/test/task_execute_test.rb +233 -0
  124. data/test/task_test.rb +424 -0
  125. data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
  126. data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
  127. data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
  128. data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
  129. data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
  130. data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
  131. data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
  132. data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
  133. data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
  134. data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
  135. data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
  136. data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
  137. data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
  138. data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
  139. data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
  140. data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
  141. data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
  142. data/test/test/inference_methods_test.rb +311 -0
  143. data/test/test/subset_methods_test.rb +115 -0
  144. data/test/test_test.rb +233 -0
  145. data/test/workflow_test.rb +108 -0
  146. metadata +274 -0
@@ -0,0 +1,5 @@
1
+ # This clears the built-in prerequisite 'db:test:prepare' from all the test tasks, allowing
2
+ # rails to run without a database (updated from "Rails Recipies":http://media.pragprog.com/titles/fr_rr/NoDatabase.pdf)
3
+ ['test:units', 'test:functionals', 'test:recent', 'test:integration', 'test:uncommitted'].each do |name|
4
+ Rake::Task[name].prerequisites.clear
5
+ end
@@ -0,0 +1,53 @@
1
+ ENV["RAILS_ENV" ] = "test"
2
+ require File.expand_path(File.dirname(__FILE__) + "/../config/environment" )
3
+
4
+ #################
5
+ # Here I've copied the contents of 'test_help' so I could remove the dependencies
6
+ # causing rails to raise errors if running without a database.
7
+ #
8
+ # require 'test_help'
9
+ require_dependency 'application'
10
+
11
+ # Make double-sure the RAILS_ENV is set to test,
12
+ # so fixtures are loaded to the right database
13
+ silence_warnings { RAILS_ENV = "test" }
14
+
15
+ require 'test/unit'
16
+ #require 'active_record/fixtures'
17
+ require 'action_controller/test_process'
18
+ require 'action_controller/integration'
19
+ #require 'action_web_service/test_invoke'
20
+ require 'breakpoint'
21
+
22
+ #Test::Unit::TestCase.fixture_path = RAILS_ROOT + "/test/fixtures/"
23
+ #ActionController::IntegrationTest.fixture_path = Test::Unit::TestCase.fixture_path
24
+
25
+ #def create_fixtures(*table_names)
26
+ # Fixtures.create_fixtures(RAILS_ROOT + "/test/fixtures", table_names)
27
+ #end
28
+ #################
29
+
30
+ class Test::Unit::TestCase
31
+ # Transactional fixtures accelerate your tests by wrapping each test method
32
+ # in a transaction that's rolled back on completion. This ensures that the
33
+ # test database remains unchanged so your fixtures don't have to be reloaded
34
+ # between every test method. Fewer database queries means faster tests.
35
+ #
36
+ # Read Mike Clark's excellent walkthrough at
37
+ # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
38
+ #
39
+ # Every Active Record database supports transactions except MyISAM tables
40
+ # in MySQL. Turn off transactional fixtures in this case; however, if you
41
+ # don't care one way or the other, switching from MyISAM to InnoDB tables
42
+ # is recommended.
43
+ #self.use_transactional_fixtures = true
44
+
45
+ # Instantiated fixtures are slow, but give you @david where otherwise you
46
+ # would need people(:david). If you don't want to migrate your existing
47
+ # test cases which use the @david style and don't mind the speed hit (each
48
+ # instantiated fixtures translates to a database query per test method),
49
+ # then set this back to true.
50
+ #self.use_instantiated_fixtures = false
51
+
52
+ # Add more helper methods to be used by all tests here...
53
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'tap'
3
+ require 'tap/test'
@@ -0,0 +1,4 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
2
+
3
+ ENV["ALL"] = true # runs all subsets (see gemdev)
4
+ Dir.glob("./**/*_test.rb").each {|test| require test}
File without changes
@@ -0,0 +1,21 @@
1
+ module Tap::Generator::Generators
2
+ class TaskGenerator < Rails::Generator::NamedBase # :nodoc:
3
+ def initialize(*args)
4
+ super(*args)
5
+ @destination_root = Tap::App.instance[:root]
6
+ @app = Tap::App.instance
7
+ end
8
+
9
+ def manifest
10
+ record do |m|
11
+ lib_path = @app.relative_filepath(:root, @app[:lib])
12
+ m.directory File.join(lib_path, class_path)
13
+ m.template "task.erb", File.join(lib_path, class_name.underscore + ".rb")
14
+
15
+ test_path = @app.relative_filepath(:root, @app[:test])
16
+ m.directory File.join(test_path, class_path)
17
+ m.template "test.erb", File.join(test_path, class_name.underscore + "_test.rb")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # == Documentation
2
+ #
3
+ # === Command Line Usage
4
+ # Replace with your command line usage instructions
5
+ #
6
+ class <%= class_name %> < Tap::Task
7
+
8
+ def process(input)
9
+ # The process logic goes here.
10
+
11
+ # You can access configurations as symbols within the config hash
12
+ message = "#{config[:greeting]} #{input}!"
13
+
14
+ # Use log to record information
15
+ log :message, message
16
+
17
+ # The return of process is the task output
18
+ message
19
+ end
20
+
21
+ end
@@ -0,0 +1,29 @@
1
+ require File.join(File.dirname(__FILE__), '<%= '../' * class_nesting_depth %>tap_test_helper.rb')
2
+ require '<%= class_path.empty? ? file_name : File.join(class_path, file_name) %>'
3
+
4
+ class <%= class_name %>Test < Test::Unit::TestCase
5
+ acts_as_tap_test
6
+
7
+ def test_greeting
8
+ t = <%= class_name %>.new nil, :greeting => 'hello'
9
+
10
+ with_options(:quiet => true) do
11
+ t.execute("from me", "to you")
12
+
13
+ assert_equal({:greeting => 'hello'}, t.config)
14
+ assert_outputs(t => ["hello from me!", "hello to you!"])
15
+
16
+ # check the audit trail for each input
17
+ assert_audits(t.results,
18
+ 0 => [[nil, 'from me'], [t, 'hello from me!']],
19
+ 1 => [[nil, 'to you'], [t, 'hello to you!']])
20
+
21
+ # an alternate way of checking audits
22
+ check_audit(t.results[1]) do
23
+ 'to you'._from nil
24
+ 'hello to you!'._from t
25
+ end
26
+ end
27
+ end
28
+
29
+ end
File without changes
@@ -0,0 +1,16 @@
1
+ # == Documentation
2
+ #
3
+ # === Command Line Usage
4
+ # Replace with your command line usage instructions
5
+ #
6
+ class <%= class_name %> < Tap::Workflow
7
+ protected
8
+
9
+ def workflow
10
+ # Define the workflow entry and exit points,
11
+ # as well as the workflow logic.
12
+ self.entry_point = Task.new
13
+
14
+ # app.sequence(entry_point, 'another/task')
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), '<%= '../' * class_nesting_depth %>tap_test_helper.rb')
2
+ require '<%= class_path.empty? ? file_name : File.join(class_path, file_name) %>'
3
+
4
+ class <%= class_name %>Test < Test::Unit::TestCase
5
+ acts_as_tap_test
6
+
7
+ end
@@ -0,0 +1,21 @@
1
+ module Tap::Generator::Generators
2
+ class WorkflowGenerator < Rails::Generator::NamedBase # :nodoc:
3
+ def initialize(*args)
4
+ super(*args)
5
+ @destination_root = Tap::App.instance[:root]
6
+ @app = Tap::App.instance
7
+ end
8
+
9
+ def manifest
10
+ record do |m|
11
+ lib_path = @app.relative_filepath(:root, @app[:lib])
12
+ m.directory File.join(lib_path, class_path)
13
+ m.template "task.erb", File.join(lib_path, class_name.underscore + ".rb")
14
+
15
+ test_path = @app.relative_filepath(:root, @app[:test])
16
+ m.directory File.join(test_path, class_path)
17
+ m.template "test.erb", File.join(test_path, class_name.underscore + "_test.rb")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Tap
2
+ module Generator
3
+ module Options # :nodoc:
4
+ protected
5
+
6
+ # Adapted from code in 'rails/rails_generator/options.rb'
7
+ def add_general_options!(opt)
8
+ opt.separator ''
9
+ opt.separator 'General Options:'
10
+
11
+ opt.on('-h', '--help', 'Show this help message and quit.') { |v| options[:help] = v }
12
+ opt.on('-p', '--pretend', 'Run but do not make any changes.') { |v| options[:pretend] = v }
13
+ opt.on('-f', '--force', 'Overwrite files that already exist.') { options[:collision] = :force }
14
+ opt.on('-s', '--skip', 'Skip files that already exist.') { options[:collision] = :skip }
15
+ opt.on('-q', '--quiet', 'Suppress normal output.') { |v| options[:quiet] = v }
16
+ opt.on('-t', '--backtrace', 'Debugging: show backtrace on errors.') { |v| options[:backtrace] = v }
17
+ opt.on('-c', '--svn', 'Modify files with subversion. (Note: svn must be in path)') do
18
+ options[:svn] = `svn status`.inject({}) do |opt, e|
19
+ opt[e.chomp[7..-1]] = true
20
+ opt
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ require 'tap/generator/options'
2
+
3
+ module Tap
4
+ module Generator
5
+ module Usage # :nodoc:
6
+ include Options
7
+
8
+ protected
9
+
10
+ # Adapted from code in 'rails/rails_generator/scripts.rb'
11
+ def usage_message
12
+ usage = "\nInstalled Generators\n"
13
+ Rails::Generator::Base.sources.each do |source|
14
+ label = source.label.to_s.capitalize
15
+ names = source.names
16
+ usage << " #{label}: #{names.join(', ')}\n" unless names.empty?
17
+ end
18
+
19
+ usage << <<end_blurb
20
+
21
+ end_blurb
22
+ return usage
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/tap/root.rb ADDED
@@ -0,0 +1,275 @@
1
+ require 'fileutils'
2
+ require 'tap/support/versions'
3
+
4
+ # string.rb is required for camelizing and underscoring strings
5
+ # requiring just this file rather than the full gem results in
6
+ # noticably faster startup
7
+ require 'active_support/core_ext/string'
8
+
9
+ module Tap
10
+
11
+ # Most applications that utilize files allow the definition of a root directory,
12
+ # allowing you to place the application directory wherever you like. Root takes
13
+ # this concept further, allowing you to define subdirectories, as well as manipulate
14
+ # files and filepaths within these directories. For example:
15
+ #
16
+ # root = Root.new 'root_dir', :input => 'in', :output => 'out'
17
+ #
18
+ # # work with directories
19
+ # root[:in] # => 'root_dir/in'
20
+ # root[:out] # => 'root_dir/out'
21
+ # root[:implicit] # => 'root_dir/implicit'
22
+ #
23
+ # # work with filepaths
24
+ # fp = root.filepath(:in, 'path/to/file.txt') # => 'root_dir/in/path/to/file.txt'
25
+ # root.relative_filepath(:in, fp) # => 'path/to/file.txt'
26
+ # root.translate(fp, :in, :out) # => 'root_dir/out/path/to/file.txt'
27
+ #
28
+ # # version filepaths
29
+ # root.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
30
+ # root.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
31
+ # root.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
32
+ #
33
+ # # work with modules...
34
+ # mp = root.module_filepath(:lib, 'Some::Module') # => 'root_dir/lib/some/module.rb'
35
+ #
36
+ # # loading them...
37
+ # Object.const_defined?(:Some) # => false
38
+ # root.lookup_module('Some::Module') # => Some::Module
39
+ # Some.const_defined?(:Module) # => true
40
+ #
41
+ # # and unloading them!
42
+ # root.unload_modules # => ["Some::Module"]
43
+ # Object.const_defined?(:Some) # => false
44
+ #
45
+ # === Notes
46
+ #
47
+ # Root internally stores paths for both relative directories and absolute paths in a
48
+ # hash for quick lookup using the dir keys. Root keeps a separate hash for the
49
+ # original directories used to create the paths, but this is not necessary for
50
+ # absolute paths. An absolute path can be distinguished from a directory by having
51
+ # an entry in paths but not directories.
52
+ class Root
53
+ class << self
54
+ include Support::Versions
55
+
56
+ # Returns the filepath of path relative to dir. If strict=true (the default) then
57
+ # relative_filepath raises an error if the path is not relative to dir.
58
+ #
59
+ # Root.relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
60
+ #
61
+ def relative_filepath(dir, path, strict=true)
62
+ dir = File.expand_path(dir)
63
+ expanded = File.expand_path(path)
64
+
65
+ unless expanded[0...dir.length] == dir
66
+ raise "\n#{expanded}\nis not relative to:\n#{dir}" if strict
67
+ return path
68
+ end
69
+
70
+ # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
71
+ # as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
72
+ # empty string is returned
73
+ expanded[(dir.length+1)..-1] || ""
74
+ end
75
+
76
+ # Lists all unique paths matching the input glob patterns.
77
+ def glob(*patterns)
78
+ patterns.collect do |pattern|
79
+ Dir.glob(pattern)
80
+ end.flatten.uniq
81
+ end
82
+
83
+ # Lists all unique versions of path matching the glob version patterns. If no patterns
84
+ # are specified, then all versions of path will be returned.
85
+ def vglob(path, *vpatterns)
86
+ vpatterns << "*" if vpatterns.empty?
87
+ vpatterns.collect do |vpattern|
88
+ results = Dir.glob(version(path, vpattern))
89
+
90
+ # extra work to include the default version path for any version
91
+ results << path if vpattern == "*" && File.exists?(path)
92
+ results
93
+ end.flatten.uniq
94
+ end
95
+
96
+
97
+
98
+ #def package(target, *filepaths)
99
+ # options = filepaths.pop if filepaths.last.kind_of?(Hash)
100
+ #end
101
+
102
+ # glob for dirs, remove if empty. removes dir if empty on complete
103
+ # OR
104
+ # remove files older than a certain date
105
+ # remove all files
106
+ #
107
+ #def cleanup(dir)
108
+ #end
109
+
110
+ # iotask?
111
+ def parse_by_line(path, options={}, &block)
112
+ str = read(path, options.merge(:as => :txt))
113
+ lines = str.split(/\r?\n/)
114
+ lines.each do |line|
115
+ # don't yield empty lines because they split to [] (ie [nil, nil] for the block inputs)
116
+ yield(line.split(/#/, 2)) unless line.empty?
117
+ end
118
+ end
119
+ end
120
+
121
+ include Support::Versions
122
+ attr_reader :root, :directories, :paths
123
+
124
+ # Creates a new Root with the given root, directories, and absolute
125
+ # paths. By default root is the current working directory and no
126
+ # directories or absolute paths are specified.
127
+ def initialize(root=Dir.pwd, directories={}, absolute_paths={})
128
+ assign_paths(root, directories, absolute_paths)
129
+ end
130
+
131
+ # Sets the root directory. All paths are reassigned accordingly.
132
+ def root=(path)
133
+ assign_paths(path, directories, absolute_paths)
134
+ end
135
+
136
+ # Sets the directories to those provided. 'root' and :root are reserved directory
137
+ # keys and cannot be set using this method (use root= instead).
138
+ def directories=(dirs)
139
+ assign_paths(root, dirs, absolute_paths)
140
+ end
141
+
142
+ # Sets the absolute paths to those provided. 'root' and :root are reserved directory
143
+ # keys and cannot be set using this method (use root= instead).
144
+ def absolute_paths=(paths)
145
+ assign_paths(root, directories, paths)
146
+ end
147
+
148
+ # Returns the absolute paths registered with the Root.
149
+ def absolute_paths
150
+ abs_paths = {}
151
+ paths.each do |dir, path|
152
+ abs_paths[dir] = path unless directories.include?(dir) || dir.to_s == 'root'
153
+ end
154
+ abs_paths
155
+ end
156
+
157
+ # Sets the path to the specified directory, relative to the root directory.
158
+ # The root directory cannot be set with this method (use root= instead).
159
+ # Absolute filepaths can be set by specifying the absolute parameter.
160
+ #
161
+ # root[:dir] = 'path/to/dir'
162
+ # root[:root] # => 'root_dir'
163
+ # root[:dir] # => 'root_dir/path/to/dir'
164
+ #
165
+ # root[:abs, true] = '/abs/path/to/dir'
166
+ # root[:abs] # => '/abs/path/to/dir'
167
+ #
168
+ # Note - the syntax for setting an absolute filepath requires an odd use []=.
169
+ # In fact the method recieves the arguments (:dir, true, '/abs/path/to/dir')
170
+ # rather than (:dir, '/abs/path/to/dir', true), meaning that internally path
171
+ # and absolute are switched when setting an absolute filepath.
172
+ def []=(dir, path, absolute=false)
173
+ raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.to_s == 'root'
174
+
175
+ # switch the paths if absolute was provided
176
+ unless absolute == false
177
+ switch = path
178
+ path = absolute
179
+ absolute = switch
180
+ end
181
+
182
+ case
183
+ when path.nil?
184
+ @directories.delete(dir)
185
+ @paths.delete(dir)
186
+ when absolute
187
+ @directories.delete(dir)
188
+ @paths[dir] = path
189
+ else
190
+ @directories[dir] = path
191
+ @paths[dir] = File.join(root, path)
192
+ end
193
+ end
194
+
195
+ # Returns the path to the specified directory. If the path has not been set,
196
+ # then the path is inferred to be 'root/dir'.
197
+ def [](dir)
198
+ self.paths[dir] || File.join(root, dir.to_s)
199
+ end
200
+
201
+ # Constructs filepaths relative to the path of the specfied directory.
202
+ def filepath(dir, *filename)
203
+ File.join(self[dir], *filename)
204
+ end
205
+
206
+ # Retrieves filepath relative to the path of the specified directory.
207
+ def relative_filepath(dir, filepath, strict=true)
208
+ Root.relative_filepath(self[dir], filepath, strict)
209
+ end
210
+
211
+ # Generates a target filepath translated from the input directory to the output directory.
212
+ #
213
+ # translate("/input/dir/path/to/input.txt") # => "/output/dir/path/to/input.txt"
214
+ def translate(filepath, input_dir, output_dir)
215
+ filepath(output_dir, relative_filepath(input_dir, filepath))
216
+ end
217
+
218
+ # Lists all files in the specified dir matching the input patterns. Patterns should be valid inputs
219
+ # for +Dir.glob+. If no patterns are specified, lists all files/folders matching '**/*'.
220
+ def glob(dir, *patterns)
221
+ patterns << "**/*" if patterns.empty?
222
+ patterns.collect! {|pattern| filepath(dir, pattern)}
223
+ Root.glob(*patterns)
224
+ end
225
+
226
+ # Lists all versions of filename in the specified dir matching the version patterns.
227
+ # If no patterns are specified, then all versions of filename will be returned.
228
+ def vglob(dir, filename, *vpatterns)
229
+ Root.vglob(filepath(dir, filename), *vpatterns)
230
+ end
231
+
232
+ # Opens the file specfied by dir and filename, then passes the open file to the input block
233
+ def open(dir, filename, mode_string="r", &block)
234
+ File.open(filepath(dir, filename), mode_string, &block)
235
+ end
236
+
237
+ # Reads the specififed file
238
+ def read(dir, filename)
239
+ File.read( filepath(dir, filename) )
240
+ end
241
+
242
+ #
243
+ def parse_by_line(dir, filename, options={}, &block)
244
+ Root.parse_by_line( filepath(dir, filename), options, &block)
245
+ end
246
+
247
+ # The timestamp of the most recently modified file under dir. Returns nil
248
+ # if none of the listed files are relative to dir.
249
+ def mtime(dir, *filenames)
250
+ filenames.collect do |fname|
251
+ fname = relative_filepath(dir, fname, false)
252
+ path = filepath(dir, fname)
253
+
254
+ File.exists?(path) ? File.mtime(path) : nil
255
+ end.compact.max
256
+ end
257
+
258
+ # Makes all the directories specified in paths. Parent directories are created as needed.
259
+ def mkpaths
260
+ paths.values.each { |dir| FileUtils.mkdir_p(dir) }
261
+ end
262
+
263
+ private
264
+
265
+ def assign_paths(root, directories, absolute_paths)
266
+ @root = root
267
+ @directories = {}
268
+ @paths = {'root' => root, :root => root}
269
+
270
+ directories.each_pair {|dir, path| self[dir] = path }
271
+ absolute_paths.each_pair {|dir, path| self[dir, true] = path }
272
+ end
273
+
274
+ end
275
+ end