diff --git a/url/src/lib.rs b/url/src/lib.rs index fa2803681..8b2b4804a 100644 --- a/url/src/lib.rs +++ b/url/src/lib.rs @@ -2756,6 +2756,72 @@ impl Url { Err(()) } + /// Append path segments to the path of a Url, escaping if necessary. + /// + /// 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. Note that + /// this clones the URL, so if you want to mutate the URL in place, use [`Url::append_path_mut`] instead. + /// + /// Examples: + /// + /// ``` + /// # use url::Url; + /// let mut my_url = Url::parse("http://www.example.com/api/v1").unwrap(); + /// my_url.append_path_mut("system/status").unwrap(); + /// assert_eq!(my_url.as_str(), "http://www.example.com/api/v1/system/status"); + /// ``` + /// + /// # Errors + /// + /// Fails if the Url is cannot-be-a-base. + #[allow(clippy::result_unit_err)] + #[inline] + pub fn append_path(&self, path: impl AsRef) -> Result { + let mut url = self.clone(); + url.append_path_mut(path)?; + Ok(url) + } + + /// Append path segments to the path of a Url, escaping if necessary. + /// + /// 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. + /// + /// This mutates the URL in place. For a non-mutating variant that returns + /// a new `Url`, use [`Url::append_path`]. + /// + /// Examples: + /// + /// ``` + /// # use url::Url; + /// let mut my_url = Url::parse("http://www.example.com/api/v1").unwrap(); + /// my_url.append_path_mut("system/status").unwrap(); + /// assert_eq!(my_url.as_str(), "http://www.example.com/api/v1/system/status"); + /// ``` + /// + /// # Errors + /// + /// Fails if the Url is cannot-be-a-base. + #[allow(clippy::result_unit_err)] + #[inline] + pub fn append_path_mut(&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 828f79756..acc27fba9 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -541,6 +541,28 @@ fn append_empty_segment_then_mutate() { assert_eq!(url.to_string(), "http://localhost:6767/foo/bar?a=b"); } +#[test] +fn append_path_returns_new_url() { + let url = Url::parse("http://www.example.com/api/v1/").unwrap(); + + let appended = url.append_path("/system/status").unwrap(); + + assert_eq!(url.as_str(), "http://www.example.com/api/v1/"); + assert_eq!( + appended.as_str(), + "http://www.example.com/api/v1/system/status" + ); +} + +#[test] +fn append_path_and_append_path_mut_fail_for_cannot_be_a_base() { + let url = Url::parse("mailto:test@example.net").unwrap(); + assert!(url.append_path("x").is_err()); + + let mut url = Url::parse("mailto:test@example.net").unwrap(); + assert!(url.append_path_mut("x").is_err()); +} + #[test] /// https://github.com/servo/rust-url/issues/243 fn test_set_host() { @@ -1392,3 +1414,42 @@ fn test_parse_url_with_single_byte_control_host() { let url2 = Url::parse(url1.as_str()).unwrap(); assert_eq!(url2, url1); } + +#[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 url = Url::parse("http://test.com").unwrap(); + let url = url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + let url = Url::parse("http://test.com").unwrap(); + let url = url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + let url = Url::parse("http://test.com/").unwrap(); + let url = url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/a/b/c"); + + let url = Url::parse("http://test.com/").unwrap(); + let url = 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 url = Url::parse("http://test.com/api/v1").unwrap(); + let url = url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); + + let url = Url::parse("http://test.com/api/v1").unwrap(); + let url = url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); + + let url = Url::parse("http://test.com/api/v1/").unwrap(); + let url = url.append_path("/a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); + + let url = Url::parse("http://test.com/api/v1/").unwrap(); + let url = url.append_path("a/b/c").unwrap(); + assert_eq!(url.as_str(), "http://test.com/api/v1/a/b/c"); +}