Class: Tap::Root

Inherits:
Object show all
Extended by:
Support::Versions
Includes:
Support::Configurable, Support::Versions
Defined in:
lib/tap/root.rb

Overview

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.

# define a root directory with aliased subdirectories
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'

# 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'

# 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'

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 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.

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’

Direct Known Subclasses

App

Constant Summary collapse

WIN_ROOT_PATTERN =

Regexp to match a windows-style root filepath.

/^[A-z]:\//

Instance Attribute Summary collapse

Attributes included from Support::Configurable

#config

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::Versions

compare_versions, deversion, increment, version

Methods included from Support::Configurable

included, #initialize_copy, #reconfigure

Constructor Details

#initialize(root = Dir.pwd, directories = {}, absolute_paths = {}) ⇒ Root

Creates a new Root with the given root directory, aliased directories and absolute paths. By default root is the present working directory and no aliased directories or absolute paths are specified.



433
434
435
436
# File 'lib/tap/root.rb', line 433

def initialize(root=Dir.pwd, directories={}, absolute_paths={})
  assign_paths(root, directories, absolute_paths)
  @config = self.class.configurations.instance_config(self)
end

Instance Attribute Details

#path_rootObject (readonly)

The filesystem root, inferred from self.root (ex ‘/’ on *nix or something like ‘C:/’ on Windows).



428
429
430
# File 'lib/tap/root.rb', line 428

def path_root
  @path_root
end

#pathsObject (readonly)

A hash of (alias, expanded path) pairs for aliased subdirectories and absolute paths.



424
425
426
# File 'lib/tap/root.rb', line 424

def paths
  @paths
end

Class Method Details

.chdir(dir, mkdir = false, &block) ⇒ Object

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.



128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/tap/root.rb', line 128

def chdir(dir, mkdir=false, &block)
  dir = File.expand_path(dir)
  
  unless File.directory?(dir)
    if !File.exists?(dir) && mkdir
      FileUtils.mkdir_p(dir)
    else
      raise ArgumentError, "not a directory: #{dir}"
    end
  end
  
  Dir.chdir(dir, &block)
end

.expanded_path?(path, root_type = path_root_type) ⇒ Boolean

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.

Root.expanded_path?('C:/path')  # => true
Root.expanded_path?('c:/path')  # => true
Root.expanded_path?('D:/path')  # => true
Root.expanded_path?('path')     # => false

If root_type == :nix, then expanded? returns true if the path begins with ‘/’.

Root.expanded_path?('/path')  # => true
Root.expanded_path?('path')   # => false

Otherwise expanded_path? always returns nil.

Returns:

  • (Boolean)


170
171
172
173
174
175
176
177
178
179
# File 'lib/tap/root.rb', line 170

def expanded_path?(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

.glob(*patterns) ⇒ Object

Lists all unique paths matching the input glob patterns.



97
98
99
100
101
# File 'lib/tap/root.rb', line 97

def glob(*patterns)
  patterns.collect do |pattern| 
    Dir.glob(pattern)
  end.flatten.uniq
end

.minimal_match?(path, mini_path) ⇒ Boolean

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 version and extname)

For example:

Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file')           # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'dir/file')       # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0')     # => true
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb')  # => true

Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file.rb')        # => false
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0')     # => false
Tap::Root.minimal_match?('dir/file-0.1.0.rb', 'another')        # => false

In matching, partial basenames are not allowed but partial directories are allowed. Hence:

Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'file')          # => true
Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'ile')           # => false
Tap::Root.minimal_match?('dir/file-0.1.0.txt', 'r/file')        # => true

Returns:

  • (Boolean)


297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/tap/root.rb', line 297

def minimal_match?(path, mini_path)
  extname = non_version_extname(mini_path)
  version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
   
  match_path = case
  when !extname.empty?
    # force full match
    path
  when !version.empty?
    # match up to version
    path.chomp(non_version_extname(path))
  else
    # match up base
    path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
  end
  
  # key ends with pattern AND basenames of each are equal... 
  # the last check ensures that a full path segment has 
  # been specified
  match_path[-mini_path.length, mini_path.length] == mini_path  && File.basename(match_path) == File.basename(mini_path)
end

.minimize(paths) ⇒ Object

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']
# => ['a', 'b']

Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
# => ['a', 'b']

Tap::Root.minimize ['path/to/file.rb', 'path/to/file.txt']
# => ['file.rb', 'file.txt']

Tap::Root.minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
# => ['path-0.1/to/file', 'path-0.2/to/file']

Minimized paths that carry their extension will always carry their version as well, but the converse is not true; paths can be minimized to carry just the version and not the path extension.

Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
# => ['a-0.1.0.rb', 'a-0.1.0.txt']

Tap::Root.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
# => ['a-0.1.0', 'a-0.2.0']

If a block is given, each (path, mini-path) pair will be passed to it after minimization.



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/tap/root.rb', line 210

def minimize(paths) # :yields: path, mini_path
  unless block_given?
    mini_paths = []
    minimize(paths) {|p, mp| mini_paths << mp }
    return mini_paths  
  end
  
  splits = paths.uniq.collect do |path|
    extname = File.extname(path)
    extname = '' if extname =~ /^\.\d+$/
    base = File.basename(path.chomp(extname))
    version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
    
    [dirname_or_array(path), base.chomp(version), extname, version, false, path]
  end

  while !splits.empty?
    index = 0
    splits = splits.collect do |(dir, base, extname, version, flagged, path)|
      index += 1
      case
      when !flagged && just_one?(splits, index, base)
        
        # found just one
        yield(path, base)
        nil
      when dir.kind_of?(Array)
        
        # no more path segments to use, try to add
        # back version and extname
        if dir.empty?
          dir << File.dirname(base)
          base = File.basename(base)
        end
        
        case
        when !version.empty?
          # add back version (occurs first)
          [dir, "#{base}#{version}", extname, '', false, path]
          
        when !extname.empty?
          
          # add back extension (occurs second)
          [dir, "#{base}#{extname}", '', version, false, path]
        else
          
          # nothing more to distinguish... path is minimized (occurs third)
          yield(path, min_join(dir[0], base))
          nil
        end
      else

        # shift path segment.  dirname_or_array returns an
        # array if this is the last path segment to shift.
        [dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
      end
    end.compact
  end
end

.path_root_typeObject

The path root type indicating windows, *nix, or some unknown style of filepaths (:win, :nix, :unknown).



144
145
146
147
148
149
150
# File 'lib/tap/root.rb', line 144

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

.relative_filepath(dir, path, dir_string = Dir.pwd) ⇒ Object

Returns the filepath of path relative to dir. Both dir and path are expanded before the relative filepath is determined. Returns nil if the path is not relative to dir.

Root.relative_filepath('dir', "dir/path/to/file.txt")  # => "path/to/file.txt"


71
72
73
74
75
76
77
78
79
80
81
# File 'lib/tap/root.rb', line 71

def relative_filepath(dir, path, dir_string=Dir.pwd)
  expanded_dir = File.expand_path(dir, dir_string)
  expanded_path = File.expand_path(path, dir_string)
  
  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] || ""
end

.sglob(suffix_pattern, *base_paths) ⇒ Object

Path suffix glob. Globs along the base paths for paths that match the specified suffix pattern.



118
119
120
121
122
123
# File 'lib/tap/root.rb', line 118

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

.split(path, expand_path = true, expand_dir = Dir.pwd) ⇒ Object

Returns the path segments for the given path, splitting along the path divider. Root paths are always represented by a string, if only an empty string.

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.

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"]

On *nix (or more generally systems with ‘/’ roots):

Dir.pwd                                               # => '/'
Root.split('path/to/.././to/file')                    # => ["", "path", "to", "file"]
Root.split('path/to/.././to/file', false)             # => ["path", "to", "file"]


343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/tap/root.rb', line 343

def split(path, expand_path=true, expand_dir=Dir.pwd)
  path = if expand_path
    File.expand_path(path, expand_dir)
  else
    # normalize the path by expanding it, then
    # work back to the relative filepath as needed
    expanded_dir = File.expand_path(expand_dir)
    expanded_path = File.expand_path(path, expand_dir)
    expanded_path.index(expanded_dir) != 0 ? expanded_path : Tap::Root.relative_filepath(expanded_dir, expanded_path)
  end

  segments = path.scan(/[^\/]+/)

  # add back the root filepath as needed on *nix 
  segments.unshift "" if path[0] == ?/
  segments
end

.translate(path, source_dir, target_dir) ⇒ Object

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'


89
90
91
92
93
94
# File 'lib/tap/root.rb', line 89

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

.vglob(path, *vpatterns) ⇒ Object

Lists all unique versions of path matching the glob version patterns.

If no patterns are specified, then all versions of path will be returned.



105
106
107
108
109
110
111
112
113
114
# File 'lib/tap/root.rb', line 105

def vglob(path, *vpatterns)
  vpatterns << "*" if vpatterns.empty?
  vpatterns.collect do |vpattern| 
    results = Dir.glob(version(path, vpattern)) 
    
    # extra work to include the default version path for any version
    results << path if vpattern == "*" && File.exists?(path)
    results
  end.flatten.uniq
end

Instance Method Details

#[](dir) ⇒ Object

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.

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'


526
527
528
529
530
531
532
# File 'lib/tap/root.rb', line 526

def [](dir)
  path = self.paths[dir] 
  return path unless path == nil
  
  dir = dir.to_s 
  Root.expanded_path?(dir) ? dir : File.expand_path(File.join(root, dir))
end

#[]=(dir, path, absolute = false) ⇒ Object

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.

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'

– Implementation Notes: 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. ++

Raises:

  • (ArgumentError)


491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/tap/root.rb', line 491

def []=(dir, path, absolute=false)
  raise ArgumentError, "The directory key '#{dir}' is reserved." if dir.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)
  when absolute
    @directories.delete(dir)
    @paths[dir] = File.expand_path(path)
  else
    @directories[dir] = path
    @paths[dir] = File.expand_path(File.join(root, path))
  end 
end

#absolute_pathsObject

Returns the absolute paths registered with self.



464
465
466
467
468
469
470
# File 'lib/tap/root.rb', line 464

def absolute_paths
  abs_paths = {}
  paths.each do |da, path| 
    abs_paths[da] = path unless directories.include?(da) || da.to_s == 'root'
  end
  abs_paths
end

#absolute_paths=(paths) ⇒ Object

Sets the absolute paths to those provided. ‘root’ and :root are reserved directory keys and cannot be set using this method (use root= instead).

r # => File.join(r.root, ‘abs’) r.absolute_paths = => ‘/path/to/dir’ r # => ‘/path/to/dir’



459
460
461
# File 'lib/tap/root.rb', line 459

def absolute_paths=(paths)
  assign_paths(root, directories, paths)
end

#chdir(dir, mkdir = false, &block) ⇒ Object

chdirs to the specified directory using Root.chdir.



571
572
573
# File 'lib/tap/root.rb', line 571

def chdir(dir, mkdir=false, &block)
  Root.chdir(self[dir], mkdir, &block)
end

#directories=(dirs) ⇒ Object

Sets the directories to those provided. ‘root’ and :root are reserved and cannot be set using this method (use root= instead).

r # => File.join(r.root, ‘alt’) r.directories = => ‘dir’ r # => File.join(r.root, ‘dir’)



449
450
451
# File 'lib/tap/root.rb', line 449

def directories=(dirs)
  assign_paths(root, dirs, absolute_paths)
end

#filepath(dir, *filename) ⇒ Object

Constructs expanded filepaths relative to the path of the specified alias.



535
536
537
538
# File 'lib/tap/root.rb', line 535

def filepath(dir, *filename)
  # TODO - consider filename.compact so nils will not raise errors
  File.expand_path(File.join(self[dir], *filename))
end

#glob(dir, *patterns) ⇒ Object

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 ‘*/’.



558
559
560
561
562
# File 'lib/tap/root.rb', line 558

def glob(dir, *patterns)
  patterns << "**/*" if patterns.empty?
  patterns.collect! {|pattern| filepath(dir, pattern)}
  Root.glob(*patterns)
end

#relative_filepath(dir, filepath) ⇒ Object

Retrieves the filepath relative to the path of the specified alias.



541
542
543
# File 'lib/tap/root.rb', line 541

def relative_filepath(dir, filepath)
  Root.relative_filepath(self[dir], filepath)
end

#root=(path) ⇒ Object

Sets the root directory. All paths are reassigned accordingly.



439
440
441
# File 'lib/tap/root.rb', line 439

def root=(path)
  assign_paths(path, directories, absolute_paths)
end

#translate(filepath, source_dir, target_dir) ⇒ Object

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.

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'


551
552
553
# File 'lib/tap/root.rb', line 551

def translate(filepath, source_dir, target_dir)
  Root.translate(filepath, self[source_dir], self[target_dir])
end

#vglob(dir, filename, *vpatterns) ⇒ Object

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.



566
567
568
# File 'lib/tap/root.rb', line 566

def vglob(dir, filename, *vpatterns)
  Root.vglob(filepath(dir, filename), *vpatterns)
end