use std::borrow::Cow;
use std::path::{Component, Path};
pub fn path_to_url(relative: &Path) -> String {
let mut url = String::new();
for component in relative.components() {
let piece: Cow<'_, str> = match component {
Component::Normal(name) => name.to_string_lossy(),
Component::ParentDir => Cow::Borrowed(".."),
Component::Prefix(_) | Component::RootDir | Component::CurDir => continue,
};
if !url.is_empty() {
url.push('/');
}
url.push_str(&piece);
}
url
}
pub fn serialize_as_url<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&path_to_url(path))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn native(parts: &[&str]) -> PathBuf {
parts.iter().collect()
}
#[test]
fn test_empty_path_is_empty_url() {
assert_eq!(path_to_url(Path::new("")), "");
}
#[test]
fn test_single_component() {
assert_eq!(path_to_url(Path::new("README.md")), "README.md");
}
#[test]
fn test_nested_path_uses_forward_slashes() {
let path = native(&["docs", "guide", "index.md"]);
let url = path_to_url(&path);
assert_eq!(url, "docs/guide/index.md");
assert!(
!url.contains('\\'),
"URL must never contain a backslash, got {url}"
);
}
#[test]
fn test_deeply_nested_path() {
let path = native(&["a", "b", "c", "d", "e", "f.png"]);
assert_eq!(path_to_url(&path), "a/b/c/d/e/f.png");
}
#[test]
fn test_parent_dir_is_preserved() {
let path = native(&["..", "..", "images", "photo.png"]);
assert_eq!(path_to_url(&path), "../../images/photo.png");
}
#[test]
fn test_current_dir_is_dropped() {
assert_eq!(path_to_url(Path::new(".")), "");
let path = native(&[".", "docs", "guide.md"]);
assert_eq!(path_to_url(&path), "docs/guide.md");
}
#[test]
fn test_root_component_is_dropped() {
assert_eq!(path_to_url(Path::new("/docs/guide.md")), "docs/guide.md");
}
#[test]
fn test_spaces_and_unicode_preserved() {
let path = native(&["my images", "café \u{1f600}.png"]);
assert_eq!(path_to_url(&path), "my images/café \u{1f600}.png");
}
#[test]
fn test_no_trailing_or_leading_slash() {
let url = path_to_url(&native(&["docs", "guide.md"]));
assert!(!url.starts_with('/'));
assert!(!url.ends_with('/'));
}
#[cfg(windows)]
#[test]
fn test_windows_backslash_literal_is_converted() {
assert_eq!(path_to_url(Path::new(r"docs\guide.md")), "docs/guide.md");
assert_eq!(
path_to_url(Path::new(r"C:\notes\docs\guide.md")),
"notes/docs/guide.md"
);
}
#[cfg(unix)]
#[test]
fn test_unix_backslash_is_a_filename_character() {
assert_eq!(path_to_url(Path::new(r"weird\name.md")), r"weird\name.md");
}
}