From fa121640f2f60bcc96684c67a20420eee066ae04 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 14 Aug 2026 19:23:06 +1200 Subject: [PATCH] Add relative URL conversion. --- lib/protocol/url/absolute.rb | 7 +++++++ lib/protocol/url/relative.rb | 17 +++++++++++++++++ releases.md | 4 ++++ test/protocol/url/absolute.rb | 8 ++++++++ test/protocol/url/relative.rb | 31 +++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+) diff --git a/lib/protocol/url/absolute.rb b/lib/protocol/url/absolute.rb index a5d992c..0b11155 100644 --- a/lib/protocol/url/absolute.rb +++ b/lib/protocol/url/absolute.rb @@ -145,6 +145,13 @@ def with(scheme: @scheme, authority: @authority, path: nil, query: @query, fragm self.class.new(scheme, authority, path || @path, query, fragment) end + # Absolute URLs cannot be made relative without comparing their origins. + # @parameter base [Object] The ignored base URL or path. + # @returns [self] This absolute URL. + def relative_to(base) + return self + end + # Convert the URL to an array representation. # # @returns [Array] An array of `[scheme, authority, path, query, fragment]`. diff --git a/lib/protocol/url/relative.rb b/lib/protocol/url/relative.rb index 33cb165..633e0e6 100644 --- a/lib/protocol/url/relative.rb +++ b/lib/protocol/url/relative.rb @@ -132,6 +132,23 @@ def with(path: nil, query: @query, fragment: @fragment, pop: true) self.class.new(path || @path, query, fragment) end + # Express this URL relative to the given base path. + # + # Already-relative paths are returned unchanged. Query and fragment components + # are preserved when converting a root-relative path. + # + # @parameter base [Relative | Path | String] The base URL or path. + # @returns [Relative] The relative URL. + def relative_to(base) + return self unless @path.absolute? + + if base.is_a?(Relative) + base = base.path + end + + return self.class.new(@path.relative(base), @query, @fragment) + end + # Normalize the encoded path and simplify its structure. # # This modifies the URL in-place by normalizing and simplifying the path component: diff --git a/releases.md b/releases.md index 8cf7be7..e217e26 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Add `Protocol::URL::Relative#relative_to` for expressing root-relative URLs relative to a base path. + ## v0.14.0 - Allow paths to compare with their encoded string representation. diff --git a/test/protocol/url/absolute.rb b/test/protocol/url/absolute.rb index 015edb5..b1d9095 100644 --- a/test/protocol/url/absolute.rb +++ b/test/protocol/url/absolute.rb @@ -199,6 +199,14 @@ end end + with "#relative_to" do + it "returns absolute URLs unchanged" do + url = Protocol::URL::Absolute.new("https", "example.com", "/docs/guide", "q=ruby", "examples") + + expect(url.relative_to("/docs/index")).to be_equal(url) + end + end + with "#to_ary" do it "returns array representation" do url = Protocol::URL::Absolute.new("https", "example.com", "/path", "q=test", "section") diff --git a/test/protocol/url/relative.rb b/test/protocol/url/relative.rb index 980349c..c292f27 100644 --- a/test/protocol/url/relative.rb +++ b/test/protocol/url/relative.rb @@ -194,6 +194,37 @@ end end + with "#relative_to" do + it "makes root-relative URLs relative to a path" do + url = Protocol::URL::Relative.new("/docs/guide", "q=ruby", "examples") + result = url.relative_to("/docs/index") + + expect(result).to be_a(Protocol::URL::Relative) + expect(result.path).to be == Protocol::URL::Path["guide"] + expect(result.query).to be == "q=ruby" + expect(result.fragment).to be == "examples" + end + + it "accepts a URL as the base" do + url = Protocol::URL::Relative.new("/docs/guide") + base = Protocol::URL::Relative.new("/docs/index") + + expect(url.relative_to(base).path).to be == Protocol::URL::Path["guide"] + end + + it "preserves encoded path segments" do + url = Protocol::URL::Relative.new("/files/a%2Fb") + + expect(url.relative_to("/index").path).to be == Protocol::URL::Path["files/a%2Fb"] + end + + it "returns already-relative URLs unchanged" do + url = Protocol::URL::Relative.new("../guide", "q=ruby", "examples") + + expect(url.relative_to("/docs/index")).to be_equal(url) + end + end + with "#to_ary" do it "returns array representation" do url = Protocol::URL::Relative.new("/path", "q=test", "section")