Skip to content
Open
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
66 changes: 66 additions & 0 deletions url/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>) -> Result<Self, ()> {
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<str>) -> 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]
Expand Down
61 changes: 61 additions & 0 deletions url/tests/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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");
}