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