Class: Tap::Root

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

Overview

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 relative paths
r = Root.new '/root_dir', :input => 'in', :output => 'out'

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

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

  • relative paths are stored in ‘relative_paths’

  • absolute paths are present in ‘paths’ but not in ‘relative_paths’

Direct Known Subclasses

App

Constant Summary collapse

WIN_ROOT_PATTERN =

Regexp to match a windows-style root filepath.

/^[A-z]:\//

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::Versions

compare_versions, deversion, increment, version

Constructor Details

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

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 relative or absolute paths are specified.



473
474
475
476
# File 'lib/tap/root.rb', line 473

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

Instance Attribute Details

#path_rootObject (readonly)

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



468
469
470
# File 'lib/tap/root.rb', line 468

def path_root
  @path_root
end

#pathsObject (readonly)

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



464
465
466
# File 'lib/tap/root.rb', line 464

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.



141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/tap/root.rb', line 141

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

.empty?(dir) ⇒ Boolean

Empty returns true when dir is an existing directory that has no files.

Returns:

  • (Boolean)


212
213
214
# File 'lib/tap/root.rb', line 212

def empty?(dir)
  File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
end

.exchange(path, extname) ⇒ Object

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'


105
106
107
# File 'lib/tap/root.rb', line 105

def exchange(path, extname)
  "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
end

.expanded?(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?('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 ‘/’.

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

Otherwise expanded? always returns nil.

Returns:

  • (Boolean)


194
195
196
197
198
199
200
201
202
203
# File 'lib/tap/root.rb', line 194

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

.glob(*patterns) ⇒ Object

Lists all unique paths matching the input glob patterns.



110
111
112
113
114
# File 'lib/tap/root.rb', line 110

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)


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/tap/root.rb', line 333

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.



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/tap/root.rb', line 245

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



169
170
171
172
173
174
175
# File 'lib/tap/root.rb', line 169

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

.prepare(path, &block) ⇒ Object

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.



160
161
162
163
164
165
# File 'lib/tap/root.rb', line 160

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

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


74
75
76
77
78
79
80
81
82
83
84
# File 'lib/tap/root.rb', line 74

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.



131
132
133
134
135
136
# File 'lib/tap/root.rb', line 131

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


380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/tap/root.rb', line 380

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'


92
93
94
95
96
97
# File 'lib/tap/root.rb', line 92

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

.trivial?(path) ⇒ Boolean

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.

Returns:

  • (Boolean)


207
208
209
# File 'lib/tap/root.rb', line 207

def trivial?(path)
  path == nil || !File.file?(path) || File.size(path) == 0
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.



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

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

#[](als) ⇒ Object

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.path_root                         # => '/'
r['relative/path']                  # => '/root_dir/relative/path'
r['/expanded/path']                 # => '/expanded/path'


571
572
573
574
575
576
577
# File 'lib/tap/root.rb', line 571

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

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

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[:abs, true] = '/abs/path/to/dir'  
r[:abs]                             # => '/abs/path/to/dir'

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

Raises:

  • (ArgumentError)


537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/tap/root.rb', line 537

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? 
    @relative_paths.delete(als)
    @paths.delete(als)
  when absolute
    @relative_paths.delete(als)
    @paths[als] = File.expand_path(path)
  else
    @relative_paths[als] = path
    @paths[als] = File.expand_path(File.join(root, path))
  end 
end

#absolute_pathsObject

Returns the absolute paths registered with self.



508
509
510
511
512
513
514
515
516
# File 'lib/tap/root.rb', line 508

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

#absolute_paths=(paths) ⇒ Object

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

r = Tap::Root.new
r['abs']                            # => File.join(r.root, 'abs')
r.absolute_paths = {'abs' => '/path/to/dir'}
r['abs']                            # => '/path/to/dir'


503
504
505
# File 'lib/tap/root.rb', line 503

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

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

Changes pwd to the specified directory using Root.chdir.



620
621
622
# File 'lib/tap/root.rb', line 620

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

#filepath(als, *paths) ⇒ Object

Resolves the specified alias, joins the paths together, and expands the resulting filepath. Analagous to File#expand_path(File#join).



581
582
583
# File 'lib/tap/root.rb', line 581

def filepath(als, *paths)
  File.expand_path(File.join(self[als], *paths))
end

#glob(als, *patterns) ⇒ Object

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



606
607
608
609
610
# File 'lib/tap/root.rb', line 606

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

#prepare(als, *paths, &block) ⇒ Object

Constructs a path from the inputs (using filepath) and prepares it using Root.prepare. Returns the path.



626
627
628
# File 'lib/tap/root.rb', line 626

def prepare(als, *paths, &block)
  Root.prepare(filepath(als, *paths), &block)
end

#relative_filepath(als, path) ⇒ Object

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



586
587
588
# File 'lib/tap/root.rb', line 586

def relative_filepath(als, path)
  Root.relative_filepath(self[als], path)
end

#relative_paths=(paths) ⇒ Object

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

r = Tap::Root.new
r['alt']                            # => File.join(r.root, 'alt')
r.relative_paths = {'alt' => 'dir'}
r['alt']                            # => File.join(r.root, 'dir')


491
492
493
# File 'lib/tap/root.rb', line 491

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

#root=(path) ⇒ Object

Sets the root directory. All paths are reassigned accordingly.



479
480
481
# File 'lib/tap/root.rb', line 479

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

#translate(path, source_als, target_als) ⇒ Object

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.

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'


598
599
600
# File 'lib/tap/root.rb', line 598

def translate(path, source_als, target_als)
  Root.translate(path, self[source_als], self[target_als])
end

#vglob(als, path, *vpatterns) ⇒ Object

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.



615
616
617
# File 'lib/tap/root.rb', line 615

def vglob(als, path, *vpatterns)
  Root.vglob(filepath(als, path), *vpatterns)
end