Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions lib/utopia/path.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2009-2025, by Samuel Williams.
# Copyright, 2009-2026, by Samuel Williams.

require "protocol/url/path"

module Utopia
# Represents a path as an array of path components. Useful for efficient URL manipulation.
# 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"]` represents `foo/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.
class Path
include Comparable

Expand Down Expand Up @@ -36,7 +44,7 @@ def empty?
@components.empty?
end

# Construct the root path.
# Construct the structural root path without an explicit trailing separator.
# @returns [Path] The root path.
def self.root
self.new([""])
Expand Down
55 changes: 53 additions & 2 deletions test/utopia/path.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
# Copyright, 2012-2026, by Samuel Williams.

require "utopia/path"

Expand All @@ -22,12 +22,27 @@
with ".root" do
let(:path) {subject.root}

it "is a root path" do
it "represents the structural root" do
expect(path).to be == [""]
expect(path).not.to be(:relative?)
expect(path).to be(:absolute?)
expect(path).to be(:directory?)
expect(path).not.to be(:file?)
expect(path).to have_attributes(local_path: be == "")
end

it "is structurally distinct from an explicit root separator" do
explicit_path = subject["/"]

expect(path).not.to be == explicit_path
expect(path.to_s).to be == explicit_path.to_s
expect(path.to_url_path).to be == explicit_path.to_url_path
end

it "traverses the structural root exactly once" do
expect(path.descend.to_a).to be == [path]
expect(path.ascend.to_a).to be == [path]
end
end

with ".create" do
Expand Down Expand Up @@ -131,6 +146,42 @@
end
end

with "root-preserving operations" do
it "does not pop the structural root" do
path = subject.root

expect(path.pop).to be_nil
expect(path).to be == subject.root
end

it "returns to the structural root after popping the final component" do
path = subject["/foo"]

expect(path.pop).to be == "foo"
expect(path).to be == subject.root
end

it "returns the structural root as the parent of a top-level component" do
expect(subject["/foo"].dirname).to be == subject.root
end

it "simplifies traversal back to the structural root" do
expect(subject["/foo/.."].simplify).to be == subject.root
end

it "uses the structural root when splitting a top-level component" do
expect(subject["/foo"].split("foo")).to be == [subject.root, subject[""]]
end

it "uses the structural root as a prefix of absolute paths" do
expect(subject["/foo/bar"].start_with?(subject.root)).to be == true
end

it "computes a parent traversal to the structural root" do
expect(subject.root.shortest_path(subject["/nested/index"])).to be == subject[".."]
end
end

with "#+" do
it "can add root path as string" do
root = Utopia::Path["/invoices/_template"]
Expand Down
Loading