lib/tap/root.rb in bahuvrihi-tap-0.11.2 vs lib/tap/root.rb in bahuvrihi-tap-0.12.0
- old
+ new
@@ -1,61 +1,64 @@
+require 'rubygems'
+require 'configurable'
require 'tap/support/versions'
-require 'tap/support/configurable'
autoload(:FileUtils, 'fileutils')
module Tap
- # Root allows you to define a root directory and alias subdirectories, so that
- # you can conceptualize what filepaths you need without predefining the full
- # filepaths. Root also simplifies operations on filepaths.
+ # Root allows you to define a root directory and alias relative paths, so
+ # that you can conceptualize what filepaths you need without predefining the
+ # full filepaths. Root also simplifies operations on filepaths.
#
- # # define a root directory with aliased subdirectories
- # r = Root.new '/root_dir', :input => 'in', :output => 'out'
+ # # define a root directory with aliased relative paths
+ # r = Root.new '/root_dir', :input => 'in', :output => 'out'
#
- # # work with directories
- # r[:input] # => '/root_dir/in'
- # r[:output] # => '/root_dir/out'
- # r['implicit'] # => '/root_dir/implicit'
+ # # work with aliases
+ # r[:input] # => '/root_dir/in'
+ # r[:output] # => '/root_dir/out'
+ # r['implicit'] # => '/root_dir/implicit'
#
- # # expanded paths are returned unchanged
- # r[File.expand_path('expanded')] # => File.expand_path('expanded')
+ # # expanded paths are returned unchanged
+ # r[File.expand_path('expanded')] # => File.expand_path('expanded')
#
- # # work with filepaths
- # fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
- # r.relative_filepath(:input, fp) # => 'path/to/file.txt'
- # r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
+ # # work with filepaths
+ # fp = r.filepath(:input, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
+ # r.relative_filepath(:input, fp) # => 'path/to/file.txt'
+ # r.translate(fp, :input, :output) # => '/root_dir/out/path/to/file.txt'
#
- # # version filepaths
- # r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
- # r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
- # r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
+ # # version filepaths
+ # r.version('path/to/config.yml', 1.0) # => 'path/to/config-1.0.yml'
+ # r.increment('path/to/config-1.0.yml', 0.1) # => 'path/to/config-1.1.yml'
+ # r.deversion('path/to/config-1.1.yml') # => ['path/to/config.yml', "1.1"]
#
- # # absolute paths can also be aliased
- # r[:abs, true] = "/absolute/path"
- # r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
+ # # absolute paths can also be aliased
+ # r[:abs, true] = "/absolute/path"
+ # r.filepath(:abs, "to", "file.txt") # => '/absolute/path/to/file.txt'
#
- # By default, Roots are initialized to the present working directory (Dir.pwd).
- # As in the 'implicit' example, Root infers a path relative to the root directory
- # whenever it needs to resolve an alias that is not explicitly set. The only
- # exceptions to this are fully expanded paths. These are returned unchanged.
+ # By default, Roots are initialized to the present working directory
+ # (Dir.pwd). As in the 'implicit' example, Root infers a path relative to
+ # the root directory whenever it needs to resolve an alias that is not
+ # explicitly set. The only exceptions to this are fully expanded paths.
+ # These are returned unchanged.
#
+ #--
# === Implementation Notes
#
- # Internally Root stores expanded paths all aliased paths in the 'paths' hash.
- # Expanding paths ensures they remain constant even when the present working
+ # Internally Root expands and stores all aliased paths in the 'paths' hash.
+ # Expanding paths ensures they remain constant even when the present working
# directory (Dir.pwd) changes.
#
- # Root keeps a separate 'directories' hash mapping aliases to their subdirectory paths.
- # This hash allow reassignment if and when the root directory changes. By contrast,
- # there is no separate data structure storing the absolute paths. An absolute path
- # thus has an alias in 'paths' but not 'directories', whereas subdirectory paths
- # have aliases in both.
+ # Root keeps a separate 'relative_paths' hash mapping aliases to their
+ # relative paths. This hash allow reassignment if and when the root directory
+ # changes. By contrast, there is no separate data structure storing the
+ # absolute paths. An absolute path thus has an alias in 'paths' but not
+ # 'relative_paths', whereas relative paths have aliases in both.
#
# These features may be important to note when subclassing Root:
# - root and all filepaths in 'paths' are expanded
- # - subdirectory paths are stored in 'directories'
- # - absolute paths are present in 'paths' but not in 'directories'
+ # - relative paths are stored in 'relative_paths'
+ # - absolute paths are present in 'paths' but not in 'relative_paths'
#
class Root
# Regexp to match a windows-style root filepath.
WIN_ROOT_PATTERN = /^[A-z]:\//
@@ -75,35 +78,45 @@
return nil unless expanded_path.index(expanded_dir) == 0
# use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
# as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
# empty string is returned
- expanded_path[( expanded_dir.chomp("/").length + 1)..-1] || ""
+ expanded_path[(expanded_dir.chomp("/").length + 1)..-1] || ""
end
- # Generates a target filepath translated from the source_dir to
- # the target_dir. Raises an error if the filepath is not relative
- # to the source_dir.
+ # Generates a target filepath translated from the source_dir to the
+ # target_dir. Raises an error if the filepath is not relative to the
+ # source_dir.
#
# Root.translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
#
def translate(path, source_dir, target_dir)
unless relative_path = relative_filepath(source_dir, path)
raise ArgumentError, "\n#{path}\nis not relative to:\n#{source_dir}"
end
File.join(target_dir, relative_path)
end
+
+ # Returns the path, exchanging the extension with extname. Extname may
+ # optionally omit the leading period.
+ #
+ # Root.exchange('path/to/file.txt', '.html') # => 'path/to/file.html'
+ # Root.exchange('path/to/file.txt', 'rb') # => 'path/to/file.rb'
+ #
+ def exchange(path, extname)
+ "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
+ end
# Lists all unique paths matching the input glob patterns.
def glob(*patterns)
patterns.collect do |pattern|
Dir.glob(pattern)
end.flatten.uniq
end
- # Lists all unique versions of path matching the glob version patterns.
- # If no patterns are specified, then all versions of path will be returned.
+ # Lists all unique versions of path matching the glob version patterns. If
+ # no patterns are specified, then all versions of path will be returned.
def vglob(path, *vpatterns)
vpatterns << "*" if vpatterns.empty?
vpatterns.collect do |vpattern|
results = Dir.glob(version(path, vpattern))
@@ -111,22 +124,22 @@
results << path if vpattern == "*" && File.exists?(path)
results
end.flatten.uniq
end
- # Path suffix glob. Globs along the base paths for
- # paths that match the specified suffix pattern.
+ # Path suffix glob. Globs along the base paths for paths that match the
+ # specified suffix pattern.
def sglob(suffix_pattern, *base_paths)
base_paths.collect do |base|
base = File.expand_path(base)
Dir.glob(File.join(base, suffix_pattern))
end.flatten.uniq
end
- # Like Dir.chdir but makes the directory, if necessary, when
- # mkdir is specified. chdir raises an error for non-existant
- # directories, as well as non-directory inputs.
+ # Like Dir.chdir but makes the directory, if necessary, when mkdir is
+ # specified. chdir raises an error for non-existant directories, as well
+ # as non-directory inputs.
def chdir(dir, mkdir=false, &block)
dir = File.expand_path(dir)
unless File.directory?(dir)
if !File.exists?(dir) && mkdir
@@ -137,49 +150,71 @@
end
Dir.chdir(dir, &block)
end
- # The path root type indicating windows, *nix, or some unknown
- # style of filepaths (:win, :nix, :unknown).
+ # Prepares the input path by making the parent directory for path. If a
+ # block is given, a file is created at path and passed to it; in this
+ # way files with non-existant parent directories are readily made.
+ #
+ # Returns path.
+ def prepare(path, &block)
+ dirname = File.dirname(path)
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
+ File.open(path, "w", &block) if block_given?
+ path
+ end
+
+ # The path root type indicating windows, *nix, or some unknown style of
+ # filepaths (:win, :nix, :unknown).
def path_root_type
@path_root_type ||= case
when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
when File.expand_path(".")[0] == ?/ then :nix
else :unknown
end
end
- # Returns true if the input path appears to be an expanded path,
- # based on Root.path_root_type.
+ # Returns true if the input path appears to be an expanded path, based on
+ # Root.path_root_type.
#
- # If root_type == :win returns true if the path matches
- # WIN_ROOT_PATTERN.
+ # If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
#
- # Root.expanded_path?('C:/path') # => true
- # Root.expanded_path?('c:/path') # => true
- # Root.expanded_path?('D:/path') # => true
- # Root.expanded_path?('path') # => false
+ # Root.expanded?('C:/path') # => true
+ # Root.expanded?('c:/path') # => true
+ # Root.expanded?('D:/path') # => true
+ # Root.expanded?('path') # => false
#
- # If root_type == :nix, then expanded? returns true if
- # the path begins with '/'.
+ # If root_type == :nix, then expanded? returns true if the path begins
+ # with '/'.
#
- # Root.expanded_path?('/path') # => true
- # Root.expanded_path?('path') # => false
+ # Root.expanded?('/path') # => true
+ # Root.expanded?('path') # => false
#
- # Otherwise expanded_path? always returns nil.
- def expanded_path?(path, root_type=path_root_type)
+ # Otherwise expanded? always returns nil.
+ def expanded?(path, root_type=path_root_type)
case root_type
when :win
path =~ WIN_ROOT_PATTERN ? true : false
when :nix
path[0] == ?/
else
nil
end
end
+ # Trivial indicates when a path does not have content to load. Returns
+ # true if the file at path is empty, non-existant, a directory, or nil.
+ def trivial?(path)
+ path == nil || !File.file?(path) || File.size(path) == 0
+ end
+
+ # Empty returns true when dir is an existing directory that has no files.
+ def empty?(dir)
+ File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
+ end
+
# Minimizes a set of paths to the set of shortest basepaths that unqiuely
# identify the paths. The path extension and versions are removed from
# the basepath if possible. For example:
#
# Tap::Root.minimize ['path/to/a.rb', 'path/to/b.rb']
@@ -265,12 +300,13 @@
end
end.compact
end
end
- # Returns true if the mini_path matches path. Matching logic
- # reverses that of minimize:
+ # Returns true if the mini_path matches path. Matching logic reverses
+ # that of minimize:
+ #
# * a match occurs when path ends with mini_path
# * if mini_path doesn't specify an extension, then mini_path
# must only match path up to the path extension
# * if mini_path doesn't specify a version, then mini_path
# must only match path up to the path basename (minus the
@@ -322,15 +358,16 @@
#
# os divider example
# windows '\' Root.split('C:\path\to\file') # => ["C:", "path", "to", "file"]
# *nix '/' Root.split('/path/to/file') # => ["", "path", "to", "file"]
#
- # The path is always expanded relative to the expand_dir; so '.' and '..' are
- # resolved. However, unless expand_path == true, only the segments relative
- # to the expand_dir are returned.
+ # The path is always expanded relative to the expand_dir; so '.' and
+ # '..' are resolved. However, unless expand_path == true, only the
+ # segments relative to the expand_dir are returned.
#
- # On windows (note that expanding paths allows the use of slashes or backslashes):
+ # On windows (note that expanding paths allows the use of slashes or
+ # backslashes):
#
# Dir.pwd # => 'C:/'
# Root.split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
# Root.split('path/to/.././to/file', false) # => ["path", "to", "file"]
#
@@ -363,10 +400,11 @@
# utility method for minimize -- joins the
# dir and path, preventing results like:
#
# "./path"
# "//path"
+ #
def min_join(dir, path) # :nodoc:
case dir
when "." then path
when "/" then "/#{path}"
else "#{dir}/#{path}"
@@ -405,188 +443,205 @@
extname = File.extname(path)
extname =~ /^\.\d+$/ ? '' : extname
end
end
-
+
+ include Configurable
include Support::Versions
- include Support::Configurable
-
+
# The root directory.
config_attr(:root, '.', :writer => false)
- # A hash of (alias, relative path) pairs for aliased subdirectories.
- config_attr(:directories, {}, :writer => false)
+ # A hash of (alias, relative path) pairs for aliased paths relative
+ # to root.
+ config_attr(:relative_paths, {}, :writer => false)
# A hash of (alias, relative path) pairs for aliased absolute paths.
config_attr(:absolute_paths, {}, :reader => false, :writer => false)
- # A hash of (alias, expanded path) pairs for aliased subdirectories and absolute paths.
+ # A hash of (alias, expanded path) pairs for expanded relative and
+ # absolute paths.
attr_reader :paths
# The filesystem root, inferred from self.root
# (ex '/' on *nix or something like 'C:/' on Windows).
attr_reader :path_root
- # Creates a new Root with the given root directory, aliased directories
+ # Creates a new Root with the given root directory, aliased relative paths
# and absolute paths. By default root is the present working directory
- # and no aliased directories or absolute paths are specified.
- def initialize(root=Dir.pwd, directories={}, absolute_paths={})
- assign_paths(root, directories, absolute_paths)
- @config = self.class.configurations.instance_config(self)
+ # and no aliased relative or absolute paths are specified.
+ def initialize(root=Dir.pwd, relative_paths={}, absolute_paths={})
+ assign_paths(root, relative_paths, absolute_paths)
+ @config = DelegateHash.new(self.class.configurations, {}, self)
end
# Sets the root directory. All paths are reassigned accordingly.
def root=(path)
- assign_paths(path, directories, absolute_paths)
+ assign_paths(path, relative_paths, absolute_paths)
end
- # Sets the directories to those provided. 'root' and :root are reserved
- # and cannot be set using this method (use root= instead).
+ # Sets the relative_paths to those provided. 'root' and :root are reserved
+ # aliases and cannot be set using this method (use root= instead).
#
- # r['alt'] # => File.join(r.root, 'alt')
- # r.directories = {'alt' => 'dir'}
- # r['alt'] # => File.join(r.root, 'dir')
- def directories=(dirs)
- assign_paths(root, dirs, absolute_paths)
+ # r = Tap::Root.new
+ # r['alt'] # => File.join(r.root, 'alt')
+ # r.relative_paths = {'alt' => 'dir'}
+ # r['alt'] # => File.join(r.root, 'dir')
+ #
+ def relative_paths=(paths)
+ assign_paths(root, paths, absolute_paths)
end
# Sets the absolute paths to those provided. 'root' and :root are reserved
- # directory keys and cannot be set using this method (use root= instead).
+ # aliases and cannot be set using this method (use root= instead).
#
- # r['abs'] # => File.join(r.root, 'abs')
- # r.absolute_paths = {'abs' => '/path/to/dir'}
- # r['abs'] # => '/path/to/dir'
+ # r = Tap::Root.new
+ # r['abs'] # => File.join(r.root, 'abs')
+ # r.absolute_paths = {'abs' => '/path/to/dir'}
+ # r['abs'] # => '/path/to/dir'
+ #
def absolute_paths=(paths)
- assign_paths(root, directories, paths)
+ assign_paths(root, relative_paths, paths)
end
# Returns the absolute paths registered with self.
def absolute_paths
abs_paths = {}
- paths.each do |da, path|
- abs_paths[da] = path unless directories.include?(da) || da.to_s == 'root'
+ paths.each do |als, path|
+ unless relative_paths.include?(als) || als.to_s == 'root'
+ abs_paths[als] = path
+ end
end
abs_paths
end
- # Sets an alias for the subdirectory relative to the root directory.
- # The aliases 'root' and :root cannot be set with this method
- # (use root= instead). Absolute filepaths can be set using the
- # second syntax.
+ # Sets an alias for the path relative to the root directory. The aliases
+ # 'root' and :root cannot be set with this method (use root= instead).
+ # Absolute filepaths can be set using the second syntax.
#
- # r = Root.new '/root_dir'
- # r[:dir] = 'path/to/dir'
- # r[:dir] # => '/root_dir/path/to/dir'
+ # r = Root.new '/root_dir'
+ # r[:dir] = 'path/to/dir'
+ # r[:dir] # => '/root_dir/path/to/dir'
#
- # r[:abs, true] = '/abs/path/to/dir'
- # r[:abs] # => '/abs/path/to/dir'
+ # r[:abs, true] = '/abs/path/to/dir'
+ # r[:abs] # => '/abs/path/to/dir'
#
#--
- # Implementation Notes:
+ # Implementation Note:
+ #
# The syntax for setting an absolute filepath requires an odd use []=.
# In fact the method recieves the arguments (:dir, true, '/abs/path/to/dir')
# rather than (:dir, '/abs/path/to/dir', true), meaning that internally path
# and absolute are switched when setting an absolute filepath.
- #++
- def []=(dir, path, absolute=false)
- raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.to_s == 'root'
+ #
+ def []=(als, path, absolute=false)
+ raise ArgumentError, "the alias #{als.inspect} is reserved" if als.to_s == 'root'
# switch the paths if absolute was provided
unless absolute == false
switch = path
path = absolute
absolute = switch
end
case
when path.nil?
- @directories.delete(dir)
- @paths.delete(dir)
+ @relative_paths.delete(als)
+ @paths.delete(als)
when absolute
- @directories.delete(dir)
- @paths[dir] = File.expand_path(path)
+ @relative_paths.delete(als)
+ @paths[als] = File.expand_path(path)
else
- @directories[dir] = path
- @paths[dir] = File.expand_path(File.join(root, path))
+ @relative_paths[als] = path
+ @paths[als] = File.expand_path(File.join(root, path))
end
end
- # Returns the expanded path for the specified alias. If the alias
- # has not been set, then the path is inferred to be 'root/dir' unless
- # the path is relative to path_root. These paths are returned
- # directly.
+ # Returns the expanded path for the specified alias. If the alias has not
+ # been set, then the path is inferred to be 'root/als'. Expanded paths
+ # are returned directly.
#
- # r = Root.new '/root_dir', :dir => 'path/to/dir'
- # r[:dir] # => '/root_dir/path/to/dir'
+ # r = Root.new '/root_dir', :dir => 'path/to/dir'
+ # r[:dir] # => '/root_dir/path/to/dir'
#
- # r.path_root # => '/'
- # r['relative/path'] # => '/root_dir/relative/path'
- # r['/expanded/path'] # => '/expanded/path'
+ # r.path_root # => '/'
+ # r['relative/path'] # => '/root_dir/relative/path'
+ # r['/expanded/path'] # => '/expanded/path'
#
- def [](dir)
- path = self.paths[dir]
+ def [](als)
+ path = self.paths[als]
return path unless path == nil
- dir = dir.to_s
- Root.expanded_path?(dir) ? dir : File.expand_path(File.join(root, dir))
+ als = als.to_s
+ Root.expanded?(als) ? als : File.expand_path(File.join(root, als))
end
- # Constructs expanded filepaths relative to the path of the specified alias.
- def filepath(dir, *filename)
- # TODO - consider filename.compact so nils will not raise errors
- File.expand_path(File.join(self[dir], *filename))
+ # Resolves the specified alias, joins the paths together, and expands the
+ # resulting filepath. Analagous to File#expand_path(File#join).
+ def filepath(als, *paths)
+ File.expand_path(File.join(self[als], *paths))
end
# Retrieves the filepath relative to the path of the specified alias.
- def relative_filepath(dir, filepath)
- Root.relative_filepath(self[dir], filepath)
+ def relative_filepath(als, path)
+ Root.relative_filepath(self[als], path)
end
- # Generates a target filepath translated from the aliased source_dir to
- # the aliased target_dir. Raises an error if the filepath is not relative
- # to the aliased source_dir.
+ # Generates a filepath translated from the aliased source dir to the
+ # aliased target dir. Raises an error if the filepath is not relative
+ # to the source dir.
#
- # fp = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
- # r.translate(fp, :in, :out) # => '/root_dir/out/path/to/file.txt'
- def translate(filepath, source_dir, target_dir)
- Root.translate(filepath, self[source_dir], self[target_dir])
+ # r = Tap::Root.new '/root_dir'
+ # path = r.filepath(:in, 'path/to/file.txt') # => '/root_dir/in/path/to/file.txt'
+ # r.translate(path, :in, :out) # => '/root_dir/out/path/to/file.txt'
+ #
+ def translate(path, source_als, target_als)
+ Root.translate(path, self[source_als], self[target_als])
end
- # Lists all files in the aliased dir matching the input patterns. Patterns
- # should be valid inputs for +Dir.glob+. If no patterns are specified, lists
- # all files/folders matching '**/*'.
- def glob(dir, *patterns)
+ # Lists all files along the aliased path matching the input patterns.
+ # Patterns should join with the aliased path make valid globs for
+ # Dir.glob. If no patterns are specified, glob returns all paths
+ # matching 'als/**/*'.
+ def glob(als, *patterns)
patterns << "**/*" if patterns.empty?
- patterns.collect! {|pattern| filepath(dir, pattern)}
+ patterns.collect! {|pattern| filepath(als, pattern)}
Root.glob(*patterns)
end
- # Lists all versions of filename in the aliased dir matching the version patterns.
- # If no patterns are specified, then all versions of filename will be returned.
- def vglob(dir, filename, *vpatterns)
- Root.vglob(filepath(dir, filename), *vpatterns)
+ # Lists all versions of path in the aliased dir matching the version
+ # patterns. If no patterns are specified, then all versions of path
+ # will be returned.
+ def vglob(als, path, *vpatterns)
+ Root.vglob(filepath(als, path), *vpatterns)
end
- # chdirs to the specified directory using Root.chdir.
- def chdir(dir, mkdir=false, &block)
- Root.chdir(self[dir], mkdir, &block)
+ # Changes pwd to the specified directory using Root.chdir.
+ def chdir(als, mkdir=false, &block)
+ Root.chdir(self[als], mkdir, &block)
end
+ # Constructs a path from the inputs (using filepath) and prepares it using
+ # Root.prepare. Returns the path.
+ def prepare(als, *paths, &block)
+ Root.prepare(filepath(als, *paths), &block)
+ end
+
private
- # reassigns all paths with the input root, directories, and absolute_paths
- def assign_paths(root, directories, absolute_paths)
+ # reassigns all paths with the input root, relative_paths, and absolute_paths
+ def assign_paths(root, relative_paths, absolute_paths)
@root = File.expand_path(root)
- @directories = {}
+ @relative_paths = {}
@paths = {'root' => @root, :root => @root}
@path_root = File.dirname(@root)
while @path_root != (parent = File.dirname(@path_root))
@path_root = parent
end
- directories.each_pair {|dir, path| self[dir] = path }
+ relative_paths.each_pair {|dir, path| self[dir] = path }
absolute_paths.each_pair {|dir, path| self[dir, true] = path }
end
end
end
\ No newline at end of file