class Path
- Includes
Comparable
Represents an application path as a traversal through a tree.
Each component names a node and / represents the edge between adjacent nodes. A leading empty component anchors the traversal at the root, while a trailing empty component preserves an explicit final edge and denotes a directory:
["foo", "bar"]representsfoo/bar.["", "foo", "bar"]represents/foo/bar.["", "foo", "bar", ""]represents/foo/bar/.
The structural root is represented by [""] and contains no traversed edge. It is intentionally distinct from parsing /, which preserves the explicit edge as ["", ""]. Both serialize as /, but they retain different structural representations. In particular, the structural root maps to an empty local path so it can be resolved relative to an application root.
Nested Classes and Modules
class MatcherPerforms structured, efficient, matches against
class Utopia::Pathinstances. Supports regular expressions, type-casts and constants.
Definitions
def initialize(components = [])
Initialize a path from its individual components.
Signature
-
parameter
componentsArray(String) The path components, including empty components that denote leading or trailing separators.
Implementation
def initialize(components = [])
@components = components
end
def freeze
Freeze this object and its internal state.
Signature
-
returns
self This object.
Implementation
def freeze
return self if frozen?
@components.freeze
super
end
def empty?
Check whether this path has no components.
Signature
-
returns
Boolean Whether the path has no components.
Implementation
def empty?
@components.empty?
end
def self.root
Construct the structural root path without an explicit trailing separator.
Signature
-
returns
Path The root path.
Implementation
def self.root
self.new([""])
end
def self.prefix_length(a, b)
Compute the number of leading components shared by two sequences.
Signature
-
parameter
aArray The first sequence.
-
parameter
bArray The second sequence.
-
returns
Integer | Nil The shared prefix length, or
nilwhen every component in the shorter sequence matches.
Implementation
def self.prefix_length(a, b)
[a.size, b.size].min.times{|i| return i if a[i] != b[i]}
end
def self.shortest_path(path, root)
Compute the shortest relative path from the containing directory of root to path.
Signature
-
parameter
pathPath | String | Array The destination path.
-
parameter
rootPath | String | Array The source path.
-
returns
Path The shortest relative path.
Implementation
def self.shortest_path(path, root)
path = self.create(path)
root = self.create(root).dirname
# Find the common prefix:
i = prefix_length(path.components, root.components) || 0
# The difference between the root path and the required path, taking into account the common prefix:
up = root.components.size - i
return self.create([".."] * up + path.components[i..-1])
end
def shortest_path(root)
Compute the shortest relative path from the containing directory of root to this path.
Signature
-
parameter
rootPath | String | Array The source path.
-
returns
Path The shortest relative path.
Implementation
def shortest_path(root)
self.class.shortest_path(self, root)
end
def self.unescape(string)
Decode URL-encoded path content, converting + to whitespace and percent-encoded bytes to their corresponding characters.
Signature
-
parameter
stringString The encoded content.
-
returns
String The decoded content.
Implementation
def self.unescape(string)
string.tr("+", " ").gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
[$1.delete("%")].pack("H*")
end
end
def self.[](path)
Coerce the given value into a path.
Signature
-
parameter
pathUtopia::Path | String The path.
-
returns
Path | Nil The coerced path.
Implementation
def self.[] path
self.create(path)
end
def [](index)
Fetch one or more path components, excluding root and directory markers from indexing.
Signature
-
parameter
indexInteger | Range The component index or range.
-
returns
String | Array(String) | Nil The selected component or components.
Implementation
def [] index
return @components[component_offset(index)]
end
def self.split(path)
Convert a path value into an array of components.
Signature
-
parameter
pathUtopia::Path | String The path.
-
returns
Array The path components.
Implementation
def self.split(path)
case path
when Path
return path.to_a
when Array
return path
when String
create(path).to_a
else
[path]
end
end
def split(at)
Split this path around a component or component index.
Signature
-
parameter
atInteger | String The component index or value at which to split.
-
returns
Array(Path, Path) | Nil The paths before and after the matched component, or
nilwhen it is not found.
Implementation
def split(at)
if at.kind_of?(String)
at = @components.index(at)
end
if at
return [self.class.new(@components[0...at]), self.class.new(@components[at+1..-1])]
else
return nil
end
end
def self.from_string(string)
Construct a path from URL-encoded text. This is an optimized direct entry point used by controller invocations.
Signature
-
parameter
stringString The encoded path.
-
returns
Path The decoded path.
Implementation
def self.from_string(string)
self.new(unescape(string).split(SEPARATOR, -1))
end
def self.load(value)
Load a path from its serialized form.
Signature
-
parameter
valueString | Nil The serialized path.
-
returns
Path | Nil The loaded path.
Implementation
def self.load(value)
from_string(value) if value
end
def self.dump(instance)
Serialize a path.
Signature
-
parameter
instancePath | Nil The path to serialize.
-
returns
String | Nil The serialized path.
Implementation
def self.dump(instance)
instance.to_s if instance
end
def self.create(path)
Coerce a value into a path.
Signature
-
parameter
pathPath | Array | String | Object | Nil The value to coerce.
-
returns
Path | Nil The coerced path.
Implementation
def self.create(path)
case path
when Path
return path
when Array
return self.new(path)
when String
return self.new(unescape(path).split(SEPARATOR, -1))
when nil
return nil
else
return self.new([path])
end
end
def replace(other_path)
Replace this path's components with a copy of another path's components.
Signature
-
parameter
other_pathPath The replacement path.
-
returns
Array(String) The copied components.
Implementation
def replace(other_path)
@components = other_path.components.dup
end
def include?(*arguments)
Check whether this collection includes the given value.
Signature
-
parameter
argumentsArray The arguments.
-
returns
Boolean Whether any component matches the given argument.
Implementation
def include?(*arguments)
@components.include?(*arguments)
end
def directory?
Check whether this path denotes a directory.
Signature
-
returns
Boolean Whether the path ends with a directory separator.
Implementation
def directory?
return @components.last == ""
end
def file?
Check whether this path denotes a file.
Signature
-
returns
Boolean Whether the path ends with a file component.
Implementation
def file?
return @components.last != ""
end
def to_directory
Convert this path to a directory path.
Signature
-
returns
Path A directory path.
Implementation
def to_directory
if directory?
return self
else
return self.class.new(@components + [""])
end
end
def relative?
Check whether this path is relative.
Signature
-
returns
Boolean Whether the path is relative.
Implementation
def relative?
@components.first != ""
end
def absolute?
Check whether this path is absolute.
Signature
-
returns
Boolean Whether the path is absolute.
Implementation
def absolute?
@components.first == ""
end
def to_absolute
Convert this path to an absolute path.
Signature
-
returns
Path An absolute path.
Implementation
def to_absolute
if absolute?
return self
else
return self.class.new([""] + @components)
end
end
def to_str
Convert this object to a string.
Signature
-
returns
String The resulting string.
Implementation
def to_str
if @components == [""]
SEPARATOR
else
@components.join(SEPARATOR)
end
end
def to_url_path
Encode this application path as a URL path.
Signature
-
returns
Protocol::URL::Path The encoded URL path.
Implementation
def to_url_path
# Preserve Utopia's compact representation of the absolute root:
if @components == [""]
return Protocol::URL::Path[SEPARATOR]
end
return Protocol::URL::Path.for(
@components,
encoding: Protocol::URL::Encoding::System,
)
end
def to_a
Convert this path to an array of components.
Signature
-
returns
Array The resulting values.
Implementation
def to_a
@components
end
def join(other)
Signature
-
parameter
otherArray(String) The path components to append.
-
returns
Path The joined and simplified path.
Implementation
def join(other)
# Check whether other is an absolute path:
if other.first == ""
self.class.new(other)
else
self.class.new(@components + other).simplify
end
end
def expand(root)
Resolve this path relative to a root path.
Signature
-
parameter
rootPath The root path.
-
returns
Path The resolved path.
Implementation
def expand(root)
root + self
end
def +(other)
Append path components and return the resulting path.
Signature
-
parameter
otherPath | Array | String | Object The value to append.
-
returns
Path The joined and simplified path, or
otherwhen it is an absolute path.
Implementation
def +(other)
if other.kind_of? Path
if other.absolute?
return other
else
return join(other.components)
end
elsif other.kind_of? Array
return join(other)
elsif other.kind_of? String
return join(other.split(SEPARATOR, -1))
else
return join([other.to_s])
end
end
def with_prefix(*arguments)
Prepend a path to this path.
Signature
-
parameter
argumentsArray The arguments accepted by
.create.-
returns
Path The prefixed path.
Implementation
def with_prefix(*arguments)
self.class.create(*arguments) + self
end
def -(other)
Computes the difference of the path. /a/b/c - /a/b -> c a/b/c - a/b -> c
Signature
-
parameter
otherPath The prefix path to remove.
-
returns
Path The remaining path.
Implementation
def -(other)
i = 0
while i < other.components.size
break if @components[i] != other.components[i]
i += 1
end
return self.class.new(@components[i,@components.size])
end
def simplify
Normalize current-directory, parent-directory, and repeated-separator components.
Signature
-
returns
Path The normalized path.
Implementation
def simplify
components = []
index = 0
if @components[0] == ""
components << ""
index += 1
end
while index < @components.size
bit = @components[index]
if bit == "."
# No-op (ignore current directory)
elsif bit == "" && index != @components.size - 1
# No-op (ignore multiple slashes)
elsif bit == ".." && components.last && components.last != ".."
if components.last != ""
# We can go up one level:
components.pop
end
else
components << bit
end
index += 1
end
return self.class.new(components)
end
def first
Return the first path component, excluding the root marker.
Signature
-
returns
String | Nil The first component.
Implementation
def first
if absolute?
@components[1]
else
@components[0]
end
end
def last
Return the last path component, excluding the root marker.
Signature
-
returns
String | Nil The last component.
Implementation
def last
if @components != [""]
@components.last
end
end
def pop
Remove the last path component without converting the root path to a relative path.
Signature
-
returns
String | Nil The removed component.
Implementation
def pop
# We don't want to convert an absolute path to a relative path.
if @components != [""]
@components.pop
end
end
def basename
Signature
-
returns
String The last path component without its file extension.
Implementation
def basename
basename, _ = @components.last.split(".", 2)
return basename || ""
end
def extension
Signature
-
returns
String | Nil The last path component's file extension.
Implementation
def extension
_, extension = @components.last.split(".", 2)
return extension
end
def dirname(count = 1)
Remove trailing path components.
Signature
-
parameter
countInteger The number of components.
-
returns
Path The containing path.
Implementation
def dirname(count = 1)
path = self.class.new(@components[0...-count])
return absolute? ? path.to_absolute : path
end
def local_path(separator = File::SEPARATOR)
Format this path using a local filesystem separator.
Signature
-
parameter
separatorString The component separator.
-
returns
String The local path.
Implementation
def local_path(separator = File::SEPARATOR)
@components.join(separator)
end
def descend(&block)
Enumerate paths from the first component down to this path.
Signature
-
yields
{|path| ...} Each successively longer path.
-
returns
Enumerator | Array An enumerator when no block is given, otherwise the component array.
Implementation
def descend(&block)
return to_enum(:descend) unless block_given?
components = []
@components.each do |component|
components << component
yield self.class.new(components.dup)
end
end
def ascend(&block)
Enumerate paths from this path up to its first component.
Signature
-
yields
{|path| ...} Each successively shorter path.
-
returns
Enumerator | Nil An enumerator when no block is given.
Implementation
def ascend(&block)
return to_enum(:ascend) unless block_given?
components = self.components.dup
while components.any?
yield self.class.new(components.dup)
components.pop
end
end
def dup
Copy this path and its component array.
Signature
-
returns
Path The copied path.
Implementation
def dup
return Path.new(components.dup)
end
def <=>(other)
Compare this object with another object.
Signature
-
parameter
otherObject The object to compare.
-
returns
Integer | Nil The comparison result.
Implementation
def <=> other
@components <=> other.components
end
def eql?(other)
Check whether this object is equivalent to another object.
Signature
-
parameter
otherObject The object to compare.
-
returns
Boolean Whether the paths have the same class and components.
Implementation
def eql? other
self.class.eql?(other.class) and @components.eql?(other.components)
end
def hash
Compute the hash value for this object.
Signature
-
returns
Integer The resulting integer.
Implementation
def hash
@components.hash
end
def ==(other)
Compare this object with another object.
Signature
-
parameter
otherObject The object to compare.
-
returns
Boolean Whether the path is equivalent to the given string, array, or path.
Implementation
def == other
return false unless other
case other
when String then self.to_s == other
when Array then self.to_a == other
else other.is_a?(self.class) && @components == other.components
end
end
def start_with?(other)
Check whether this path starts with the given path.
Signature
-
parameter
otherPath The possible prefix.
-
returns
Boolean Whether this path starts with all components of
other.
Implementation
def start_with? other
other.components.each_with_index do |part, index|
return false if @components[index] != part
end
return true
end
def []=(index, value)
Replace one or more path components using the same root- and directory-marker-aware indexing as #[].
Signature
-
parameter
indexInteger | Range The component index or range.
-
parameter
valueString | Array(String) The replacement component or components.
-
returns
String | Array(String) The assigned value.
Implementation
def []= index, value
return @components[component_offset(index)] = value
end
def delete_at(index)
Delete a path component, excluding root and directory markers from indexing.
Signature
-
parameter
indexInteger The component index.
-
returns
String | Nil The deleted component.
Implementation
def delete_at(index)
@components.delete_at(component_offset(index))
end
def component_offset(index)
We adjust the index slightly so that indices reference path components rather than the directory markers at the start and end of the path components array.
Implementation
def component_offset(index)
if Range === index
Range.new(adjust_index(index.first), adjust_index(index.last), index.exclude_end?)
else
adjust_index(index)
end
end