UtopiaSourceUtopiaPath

class Path

Represents a path as an array of path components. Useful for efficient URL manipulation.

Nested

Definitions

def initialize(components = [])

Initialize a path from its individual components.

Signature

parameter components Array(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 root path.

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 a Array

The first sequence.

parameter b Array

The second sequence.

returns Integer | Nil

The shared prefix length, or nil when 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 path Path | String | Array

The destination path.

parameter root Path | 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 root Path | 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 string String

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 path Utopia::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 index Integer | 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 path Utopia::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 at Integer | String

The component index or value at which to split.

returns Array(Path, Path) | Nil

The paths before and after the matched component, or nil when 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 string String

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 value String | 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 instance Path | 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 path Path | 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_path Path

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 arguments Array

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_relative!

Remove the first component when this path is relative.

Signature

returns String | Nil

The removed component, or nil when the path is absolute.

Implementation

def to_relative!
	@components.shift if relative?
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_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 other Array(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 root Path

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 other Path | Array | String | Object

The value to append.

returns Path

The joined and simplified path, or other when 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 arguments Array

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 other Path

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 count Integer

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 separator String

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 other Object

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 other Object

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 other Object

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 other Path

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 index Integer | Range

The component index or range.

parameter value String | 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 index Integer

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