diff --git a/lib/utopia/path.rb b/lib/utopia/path.rb index af80d641..1ea4fe04 100644 --- a/lib/utopia/path.rb +++ b/lib/utopia/path.rb @@ -39,7 +39,7 @@ def empty? # Construct the root path. # @returns [Path] The root path. def self.root - self.new([""]) + self.new(["", ""]) end # Compute the number of leading components shared by two sequences. @@ -63,8 +63,14 @@ def self.shortest_path(path, root) # The difference between the root path and the required path, taking into account the common prefix: up = root.components.size - i + down = path.components[i..-1] - return self.create([".."] * up + path.components[i..-1]) + # A parent component already denotes the destination directory, so a trailing directory separator is redundant: + if up > 0 && down == [""] + down = [] + end + + return self.create([".."] * up + down) end # Compute the shortest relative path from the containing directory of `root` to this path. @@ -135,6 +141,7 @@ def self.create(path) when Path return path when Array + return self.root if path == [""] return self.new(path) when String return self.new(unescape(path).split(SEPARATOR, -1)) @@ -199,7 +206,7 @@ def to_absolute if absolute? return self else - return self.class.new([""] + @components) + return self.class.create([""] + @components) end end @@ -212,11 +219,7 @@ def to_relative! # Convert this object to a string. # @returns [String] The resulting string. def to_str - if @components == [""] - SEPARATOR - else - @components.join(SEPARATOR) - end + @components.join(SEPARATOR) end alias to_s to_str @@ -224,11 +227,6 @@ def to_str # Encode this application path as a URL path. # @returns [Protocol::URL::Path] The encoded URL path. 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, @@ -246,7 +244,7 @@ def to_a def join(other) # Check whether other is an absolute path: if other.first == "" - self.class.new(other) + self.class.create(other) else self.class.new(@components + other).simplify end @@ -332,7 +330,7 @@ def simplify index += 1 end - return self.class.new(components) + return self.class.create(components) end # Return the first path component, excluding the root marker. @@ -348,9 +346,7 @@ def first # Return the last path component, excluding the root marker. # @returns [String | Nil] The last component. def last - if @components != [""] - @components.last - end + @components.last end alias last? file? @@ -358,10 +354,19 @@ def last # Remove the last path component without converting the root path to a relative path. # @returns [String | Nil] The removed component. def pop - # We don't want to convert an absolute path to a relative path. - if @components != [""] - @components.pop + # The absolute root has no path component to remove: + if @components == ["", ""] + return nil end + + component = @components.pop + + # Preserve the canonical root after removing the final component: + if @components == [""] + @components << "" + end + + return component end # @returns [String] The last path component without its file extension. @@ -382,7 +387,7 @@ def extension # @parameter count [Integer] The number of components. # @returns [Path] The containing path. def dirname(count = 1) - path = self.class.new(@components[0...-count]) + path = self.class.create(@components[0...-count]) return absolute? ? path.to_absolute : path end @@ -400,12 +405,17 @@ def local_path(separator = File::SEPARATOR) def descend(&block) return to_enum(:descend) unless block_given? + if @components == ["", ""] + yield self.class.root + return @components + end + components = [] @components.each do |component| components << component - yield self.class.new(components.dup) + yield self.class.create(components.dup) end end @@ -418,7 +428,10 @@ def ascend(&block) components = self.components.dup while components.any? - yield self.class.new(components.dup) + path = self.class.create(components.dup) + yield path + + break if path.components == ["", ""] components.pop end @@ -433,7 +446,7 @@ def split(at) end if at - return [self.class.new(@components[0...at]), self.class.new(@components[at+1..-1])] + return [self.class.create(@components[0...at]), self.class.create(@components[at+1..-1])] else return nil end @@ -482,6 +495,11 @@ def == other # @parameter other [Path] The possible prefix. # @returns [Boolean] Whether this path starts with all components of `other`. def start_with? other + # The root directory contains every absolute path: + if other.components == ["", ""] + return absolute? + end + other.components.each_with_index do |part, index| return false if @components[index] != part end diff --git a/test/utopia/path.rb b/test/utopia/path.rb index 3ef258f4..d7482d3c 100755 --- a/test/utopia/path.rb +++ b/test/utopia/path.rb @@ -23,10 +23,20 @@ let(:path) {subject.root} it "is a root path" do - expect(path).to be == [""] + expect(path).to be == ["", ""] + expect(path).to be == subject.create("/") + expect(path).to be == subject.create([""]) + expect(path).not.to be == subject.create("") expect(path).not.to be(:relative?) expect(path).to be(:absolute?) - expect(path).to have_attributes(local_path: be == "") + expect(path).to be(:directory?) + expect(path).not.to be(:file?) + expect(path).to have_attributes(local_path: be == "/") + end + + it "traverses the root exactly once" do + expect(path.descend.to_a).to be == [path] + expect(path.ascend.to_a).to be == [path] end end @@ -120,7 +130,7 @@ let(:path) {subject.root} it "can extract the last path component from a a root path" do - expect(path.last).to be == nil + expect(path.last).to be == "" end end @@ -131,6 +141,22 @@ end end + with "#pop" do + it "does not remove the absolute root" do + path = Utopia::Path.root + + expect(path.pop).to be_nil + expect(path).to be == Utopia::Path.root + end + + it "preserves the absolute root after removing the final component" do + path = Utopia::Path["/foo"] + + expect(path.pop).to be == "foo" + expect(path).to be == Utopia::Path.root + end + end + with "#+" do it "can add root path as string" do root = Utopia::Path["/invoices/_template"] @@ -161,6 +187,12 @@ end end + with "#to_absolute" do + it "converts an empty relative path to the absolute root" do + expect(Utopia::Path[""].to_absolute).to be == Utopia::Path.root + end + end + it "should concatenate absolute paths" do root = Utopia::Path["/"] @@ -173,7 +205,7 @@ descendants = root.descend.to_a - expect(descendants[0].components).to be == [""] + expect(descendants[0].components).to be == ["", ""] expect(descendants[1].components).to be == ["", "foo"] expect(descendants[2].components).to be == ["", "foo", "bar"] @@ -224,12 +256,30 @@ expect(path.start_with?(path.dirname)).to be == true end + it "should start with the absolute root" do + path = Utopia::Path["/a/b/c/d/e"] + + expect(path.start_with?(Utopia::Path.root)).to be == true + end + + it "should preserve the absolute root when taking the directory name" do + path = Utopia::Path["/foo"] + + expect(path.dirname).to be == Utopia::Path.root + end + it "should split at the specified point" do path = Utopia::Path["/a/b/c/d/e"] expect(path.split("c")).to be == [Utopia::Path["/a/b"], Utopia::Path["d/e"]] end + it "should preserve the absolute root when splitting the first component" do + path = Utopia::Path["/foo"] + + expect(path.split("foo")).to be == [Utopia::Path.root, Utopia::Path[""]] + end + it "shouldn't be able to modify frozen paths" do path = Utopia::Path["dir/foo.html"] @@ -287,7 +337,20 @@ expect((output + short).simplify).to be == input end + it "should omit a redundant root separator when ascending to a parent directory" do + input = Utopia::Path.root + output = Utopia::Path["/nested/index"] + + expect(input.shortest_path(output)).to be == Utopia::Path[".."] + end + with "#simplify" do + it "preserves the absolute root" do + path = Utopia::Path["/foo/.."] + + expect(path.simplify).to be == Utopia::Path.root + end + it "doesn't remove leading .. from relative paths" do path = Utopia::Path["../foo/bar"] simplified = path.simplify