aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs166
1 files changed, 155 insertions, 11 deletions
diff --git a/src/lib.rs b/src/lib.rs
index afe511e..42793cf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -120,7 +120,7 @@ url = { version = "2", features = ["serde"] }
```
*/
-#![doc(html_root_url = "https://docs.rs/url/2.2.1")]
+#![doc(html_root_url = "https://docs.rs/url/2.2.2")]
#[macro_use]
extern crate matches;
@@ -320,6 +320,8 @@ impl Url {
/// Parse a string as an URL, with this URL as the base URL.
///
+ /// The inverse of this is [`make_relative`].
+ ///
/// Note: a trailing slash is significant.
/// Without it, the last path component is considered to be a “file” name
/// to be removed to get at the “directory” that is used as the base:
@@ -349,11 +351,144 @@ impl Url {
/// with this URL as the base URL, a [`ParseError`] variant will be returned.
///
/// [`ParseError`]: enum.ParseError.html
+ /// [`make_relative`]: #method.make_relative
#[inline]
pub fn join(&self, input: &str) -> Result<Url, crate::ParseError> {
Url::options().base_url(Some(self)).parse(input)
}
+ /// Creates a relative URL if possible, with this URL as the base URL.
+ ///
+ /// This is the inverse of [`join`].
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use url::Url;
+ /// # use url::ParseError;
+ ///
+ /// # fn run() -> Result<(), ParseError> {
+ /// let base = Url::parse("https://example.net/a/b.html")?;
+ /// let url = Url::parse("https://example.net/a/c.png")?;
+ /// let relative = base.make_relative(&url);
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png"));
+ ///
+ /// let base = Url::parse("https://example.net/a/b/")?;
+ /// let url = Url::parse("https://example.net/a/b/c.png")?;
+ /// let relative = base.make_relative(&url);
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png"));
+ ///
+ /// let base = Url::parse("https://example.net/a/b/")?;
+ /// let url = Url::parse("https://example.net/a/d/c.png")?;
+ /// let relative = base.make_relative(&url);
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("../d/c.png"));
+ ///
+ /// let base = Url::parse("https://example.net/a/b.html?c=d")?;
+ /// let url = Url::parse("https://example.net/a/b.html?e=f")?;
+ /// let relative = base.make_relative(&url);
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("?e=f"));
+ /// # Ok(())
+ /// # }
+ /// # run().unwrap();
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// If this URL can't be a base for the given URL, `None` is returned.
+ /// This is for example the case if the scheme, host or port are not the same.
+ ///
+ /// [`join`]: #method.join
+ pub fn make_relative(&self, url: &Url) -> Option<String> {
+ if self.cannot_be_a_base() {
+ return None;
+ }
+
+ // Scheme, host and port need to be the same
+ if self.scheme() != url.scheme() || self.host() != url.host() || self.port() != url.port() {
+ return None;
+ }
+
+ // We ignore username/password at this point
+
+ // The path has to be transformed
+ let mut relative = String::new();
+
+ // Extract the filename of both URIs, these need to be handled separately
+ fn extract_path_filename(s: &str) -> (&str, &str) {
+ let last_slash_idx = s.rfind('/').unwrap_or(0);
+ let (path, filename) = s.split_at(last_slash_idx);
+ if filename.is_empty() {
+ (path, "")
+ } else {
+ (path, &filename[1..])
+ }
+ }
+
+ let (base_path, base_filename) = extract_path_filename(self.path());
+ let (url_path, url_filename) = extract_path_filename(url.path());
+
+ let mut base_path = base_path.split('/').peekable();
+ let mut url_path = url_path.split('/').peekable();
+
+ // Skip over the common prefix
+ while base_path.peek().is_some() && base_path.peek() == url_path.peek() {
+ base_path.next();
+ url_path.next();
+ }
+
+ // Add `..` segments for the remainder of the base path
+ for base_path_segment in base_path {
+ // Skip empty last segments
+ if base_path_segment.is_empty() {
+ break;
+ }
+
+ if !relative.is_empty() {
+ relative.push('/');
+ }
+
+ relative.push_str("..");
+ }
+
+ // Append the remainder of the other URI
+ for url_path_segment in url_path {
+ if !relative.is_empty() {
+ relative.push('/');
+ }
+
+ relative.push_str(url_path_segment);
+ }
+
+ // Add the filename if they are not the same
+ if base_filename != url_filename {
+ // If the URIs filename is empty this means that it was a directory
+ // so we'll have to append a '/'.
+ //
+ // Otherwise append it directly as the new filename.
+ if url_filename.is_empty() {
+ relative.push('/');
+ } else {
+ if !relative.is_empty() {
+ relative.push('/');
+ }
+ relative.push_str(url_filename);
+ }
+ }
+
+ // Query and fragment are only taken from the other URI
+ if let Some(query) = url.query() {
+ relative.push('?');
+ relative.push_str(query);
+ }
+
+ if let Some(fragment) = url.fragment() {
+ relative.push('#');
+ relative.push_str(fragment);
+ }
+
+ Some(relative)
+ }
+
/// Return a default `ParseOptions` that can fully configure the URL parser.
///
/// # Examples
@@ -417,14 +552,15 @@ impl Url {
/// # fn run() -> Result<(), ParseError> {
/// let url_str = "https://example.net/";
/// let url = Url::parse(url_str)?;
- /// assert_eq!(url.into_string(), url_str);
+ /// assert_eq!(String::from(url), url_str);
/// # Ok(())
/// # }
/// # run().unwrap();
/// ```
#[inline]
+ #[deprecated(since = "2.3.0", note = "use Into<String>")]
pub fn into_string(self) -> String {
- self.serialization
+ self.into()
}
/// For internal testing, not part of the public API.
@@ -1433,7 +1569,7 @@ impl Url {
/// Return an object with methods to manipulate this URL’s path segments.
///
/// Return `Err(())` if this URL is cannot-be-a-base.
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn path_segments_mut(&mut self) -> Result<PathSegmentsMut<'_>, ()> {
if self.cannot_be_a_base() {
Err(())
@@ -1517,7 +1653,7 @@ impl Url {
/// # }
/// # run().unwrap();
/// ```
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn set_port(&mut self, mut port: Option<u16>) -> Result<(), ()> {
// has_host implies !cannot_be_a_base
if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" {
@@ -1788,7 +1924,7 @@ impl Url {
/// # run().unwrap();
/// ```
///
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn set_ip_host(&mut self, address: IpAddr) -> Result<(), ()> {
if self.cannot_be_a_base() {
return Err(());
@@ -1828,7 +1964,7 @@ impl Url {
/// # }
/// # run().unwrap();
/// ```
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn set_password(&mut self, password: Option<&str>) -> Result<(), ()> {
// has_host implies !cannot_be_a_base
if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" {
@@ -1921,7 +2057,7 @@ impl Url {
/// # }
/// # run().unwrap();
/// ```
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn set_username(&mut self, username: &str) -> Result<(), ()> {
// has_host implies !cannot_be_a_base
if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" {
@@ -2163,7 +2299,7 @@ impl Url {
/// # }
/// ```
#[cfg(any(unix, windows, target_os = "redox"))]
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> {
let mut serialization = "file://".to_owned();
let host_start = serialization.len() as u32;
@@ -2200,7 +2336,7 @@ impl Url {
/// Note that `std::path` does not consider trailing slashes significant
/// and usually does not include them (e.g. in `Path::parent()`).
#[cfg(any(unix, windows, target_os = "redox"))]
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> {
let mut url = Url::from_file_path(path)?;
if !url.serialization.ends_with('/') {
@@ -2317,7 +2453,7 @@ impl Url {
/// for a Windows path, is not UTF-8.)
#[inline]
#[cfg(any(unix, windows, target_os = "redox"))]
- #[allow(clippy::clippy::result_unit_err)]
+ #[allow(clippy::result_unit_err)]
pub fn to_file_path(&self) -> Result<PathBuf, ()> {
if let Some(segments) = self.path_segments() {
let host = match self.host() {
@@ -2375,6 +2511,13 @@ impl fmt::Display for Url {
}
}
+/// String converstion.
+impl From<Url> for String {
+ fn from(value: Url) -> String {
+ value.serialization
+ }
+}
+
/// Debug the serialization of this URL.
impl fmt::Debug for Url {
#[inline]
@@ -2382,6 +2525,7 @@ impl fmt::Debug for Url {
formatter
.debug_struct("Url")
.field("scheme", &self.scheme())
+ .field("cannot_be_a_base", &self.cannot_be_a_base())
.field("username", &self.username())
.field("password", &self.password())
.field("host", &self.host())