From 41c6be6e4038d37106c9c85560046af39854deda Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Sun, 2 Jun 2024 10:46:32 -0600 Subject: [PATCH 1/2] add an "append_path" function to url this function is an alternative to `Url::join` which addresses issues discussed in #333, mainly that `Url::join` is sensitive to trailing slashes is in the `Url`, and if the trailing slash is missing, may remove segments from the base url and replace them with segments from the joined `Url`. There are good reasons for `Url::join` to behave that way, because that is was is specified in the `Url` standard. However it's still inconvenient because it often leads to situations where, a service takes some base-url for some API as a config parameter, uses `Url::join` to append various routes to it and make requests, and if a trailing `/` is omitted in a config file, you don't figure it out until deploying and looking at logs and seeing nonsense requests failing. In many situations in web development these trailing `/` are not significant so this is easy to forget and can become just an annoying papercut. One suggestion in #333 was to add an alternative utility function that isn't sensitive to the trailing `/`'s in this way. This commit adds such a utility function with tests. --- url/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ url/tests/unit.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/url/src/lib.rs b/url/src/lib.rs index 1959a1213..9bef3b8b3 100644 --- a/url/src/lib.rs +++ b/url/src/lib.rs @@ -2637,6 +2637,38 @@ impl Url { Err(()) } + /// Append path segments to the path of a Url, escaping if necesary. + /// Fails if the Url is cannot-be-a-base. + /// + /// This differs from Url::join in that it is insensitive to trailing slashes + /// in the url and leading slashes in the passed string. See documentation of Url::join for discussion + /// of this subtlety. Also, this function cannot change any part of the Url other than the path. + /// + /// Examples: + /// + /// ``` + /// # use url::Url; + /// let mut my_url = Url::parse("http://www.example.com/api/v1").unwrap(); + /// my_url.append_path("system/status").unwrap(); + /// assert_eq!(my_url.as_str(), "http://www.example.com/api/v1/system/status"); + /// ``` + #[inline] + pub fn append_path(&mut self, path: impl AsRef) -> Result<(), ()> { + // This fails if self is cannot-be-a-base but succeeds otherwise. + let mut path_segments_mut = self.path_segments_mut()?; + + // Remove the last segment if it is empty, this makes our code tolerate trailing `/`'s + path_segments_mut.pop_if_empty(); + + // Remove any leading `/` from the path we are appending, this makes our code tolerate leading `/`'s + let path = path.as_ref(); + let path = path.strip_prefix('/').unwrap_or(path); + for segment in path.split('/') { + path_segments_mut.push(segment); + } + Ok(()) + } + // Private helper methods: #[inline] diff --git a/url/tests/unit.rs b/url/tests/unit.rs index afe842beb..4dc757db9 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -1316,3 +1316,42 @@ fn issue_864() { url.set_path("x"); dbg!(&url); } + +#[test] +/// append_path is an alternative to Url::join addressing issues described in +/// https://github.com/servo/rust-url/issues/333 +fn test_append_path() { + // append_path behaves as expected when path is `/` regardless of trailing & leading slashes + let mut url = Url::parse("http://test.com").unwrap(); + url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + let mut url = Url::parse("http://test.com").unwrap(); + url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + let mut url = Url::parse("http://test.com/").unwrap(); + url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + let mut url = Url::parse("http://test.com/").unwrap(); + url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + // append_path behaves as expected when path is `/api/v1` regardless of trailing & leading slashes + let mut url = Url::parse("http://test.com/api/v1").unwrap(); + url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); + + let mut url = Url::parse("http://test.com/api/v1").unwrap(); + url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); + + let mut url = Url::parse("http://test.com/api/v1/").unwrap(); + url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); + + let mut url = Url::parse("http://test.com/api/v1/").unwrap(); + url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); +} From b8691e111e8b49bc1265248731c7a74cdac4b208 Mon Sep 17 00:00:00 2001 From: Chris Beck Date: Mon, 3 Jun 2024 10:52:01 -0600 Subject: [PATCH 2/2] fix clippy --- url/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/url/src/lib.rs b/url/src/lib.rs index 9bef3b8b3..a42c7f83a 100644 --- a/url/src/lib.rs +++ b/url/src/lib.rs @@ -2638,7 +2638,6 @@ impl Url { } /// Append path segments to the path of a Url, escaping if necesary. - /// Fails if the Url is cannot-be-a-base. /// /// This differs from Url::join in that it is insensitive to trailing slashes /// in the url and leading slashes in the passed string. See documentation of Url::join for discussion @@ -2652,6 +2651,9 @@ impl Url { /// my_url.append_path("system/status").unwrap(); /// assert_eq!(my_url.as_str(), "http://www.example.com/api/v1/system/status"); /// ``` + /// + /// Fails if the Url is cannot-be-a-base. + #[allow(clippy::result_unit_err)] #[inline] pub fn append_path(&mut self, path: impl AsRef) -> Result<(), ()> { // This fails if self is cannot-be-a-base but succeeds otherwise.