use {
crate::{
Uri,
fs::errors::{
FsError,
Result,
},
},
error_stack::Report,
};
pub trait UriPath {
fn parent(&self) -> Option<Uri>;
fn ends_with(&self, ext: &str) -> bool;
fn is_file(&self) -> bool;
fn is_dir(&self) -> bool;
fn file_name(&self) -> Option<String>;
fn path_segments_vec(&self) -> Vec<String>;
fn normalize(&self) -> Uri;
fn join_path(&self, path: &str) -> Result<Uri>;
fn strip_prefix(&self, base: &Uri) -> Uri;
fn to_scoped_string(&self) -> String;
fn has_extension(&self, ext: &str) -> bool;
fn starts_with(&self, base: &Uri) -> bool;
}
impl UriPath for Uri {
fn parent(&self) -> Option<Uri> {
let path = self.path_str();
if path == "/" {
return None;
}
let path = path.trim_end_matches('/');
if let Some(last_slash) = path.rfind('/') {
let parent_path = &path[..last_slash + 1];
self.join(parent_path)
} else {
None
}
}
fn ends_with(&self, ext: &str) -> bool {
self.path_str().ends_with(ext)
}
fn is_file(&self) -> bool {
self
.path_segments()
.and_then(|mut segments| segments.next_back())
.map(|last| last.contains('.'))
.unwrap_or(false)
}
fn is_dir(&self) -> bool {
!self.is_file()
}
fn file_name(&self) -> Option<String> {
self.path_segments()?.next_back().map(String::from)
}
fn path_segments_vec(&self) -> Vec<String> {
let mut segments = Vec::new();
if let Some(path_segments) = self.path_segments() {
segments
.extend(path_segments.map(String::from).filter(|s| !s.is_empty()));
}
segments
}
fn normalize(&self) -> Uri {
let path = self.path_str();
let mut components = Vec::new();
for component in path.split('/') {
match component {
| "" | "." => continue,
| ".." => {
components.pop();
},
| c => components.push(c),
}
}
let mut normalized = self.clone();
let normalized_path = format!("/{}", components.join("/"));
normalized.set_path(&normalized_path);
normalized
}
fn join_path(&self, path: &str) -> Result<Uri> {
self.join(path).ok_or_else(|| {
Report::new(FsError::uri_error("Failed to join uri"))
.attach_printable(format!("Base uri: {self}"))
.attach_printable(format!("Path: {path}"))
})
}
fn strip_prefix(&self, base: &Uri) -> Uri {
if let Some(relative) = self.make_relative(base) {
if relative.is_empty() {
self.clone()
} else if relative.contains("..") {
self.clone()
} else {
let authority_str = self
.authority()
.map(|a| format!("//{}", a.as_str()))
.unwrap_or_default();
let new_uri_str =
format!("{}:{}/{}", self.scheme(), authority_str, relative);
Uri::parse(&new_uri_str).unwrap_or_else(|_| self.clone())
}
} else {
self.clone()
}
}
fn to_scoped_string(&self) -> String {
let mut result = self
.path_segments_vec()
.into_iter()
.filter(|s| !s.is_empty() && !s.trim().is_empty())
.map(|s| s.trim().to_string())
.collect::<Vec<String>>()
.join("::");
if let Some(query) = self.query() {
result.push('?');
result.push_str(query.as_str());
}
result
}
fn has_extension(&self, ext: &str) -> bool {
self
.path_segments()
.and_then(|mut segments| segments.next_back())
.map(|last| last.ends_with(ext))
.unwrap_or(false)
}
fn starts_with(&self, base: &Uri) -> bool {
if self.scheme() != base.scheme() {
return false;
}
let self_auth = self.authority().map(|a| a.as_str());
let base_auth = base.authority().map(|a| a.as_str());
if self_auth != base_auth {
return false;
}
let base_path = base.path().as_str();
let self_path = self.path().as_str();
self_path.starts_with(base_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uri_parent() {
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(uri.parent().unwrap().as_str(), "file://localhost/path/to/");
}
#[test]
fn test_uri_ends_with() {
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert!(uri.ends_with(".txt"));
assert!(!uri.ends_with(".rs"));
}
#[test]
fn test_is_file_and_dir() {
let file = Uri::parse("file://localhost/path/to/file.txt").unwrap();
let dir = Uri::parse("file://localhost/path/to/").unwrap();
assert!(file.is_file());
assert!(!file.is_dir());
assert!(dir.is_dir());
assert!(!dir.is_file());
}
#[test]
fn test_file_name() {
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(uri.file_name().unwrap(), "file.txt");
}
#[test]
fn test_path_segments_vec() {
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(uri.path_segments_vec(), vec!["path", "to", "file.txt"]);
}
#[test]
fn test_normalize() {
{
let uri =
Uri::parse("file://localhost/path/./to/../to/file.txt").unwrap();
assert_eq!(
uri.normalize().as_str(),
"file://localhost/path/to/file.txt"
);
}
{
let uri = Uri::parse("file://localhost/path/./to/././././../to/file.txt")
.unwrap();
assert_eq!(
uri.normalize().as_str(),
"file://localhost/path/to/file.txt"
);
}
}
#[test]
fn test_join_path() {
let uri = Uri::parse("file://localhost/path/to/").unwrap();
let joined = uri.join_path("file.txt").unwrap();
assert_eq!(joined.as_str(), "file://localhost/path/to/file.txt");
}
#[test]
fn test_strip_prefix() {
let base = Uri::parse("file://localhost/path/").unwrap();
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(
uri.strip_prefix(&base).as_str(),
"file://localhost/to/file.txt"
);
}
#[test]
fn test_strip_prefix_same_uri() {
let base = Uri::parse("file://localhost/path/to/file.txt").unwrap();
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(
uri.strip_prefix(&base).as_str(),
"file://localhost/path/to/file.txt"
);
}
#[test]
fn test_strip_prefix_unrelated_uris() {
let base = Uri::parse("file://other/path/").unwrap();
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(
uri.strip_prefix(&base).as_str(),
"file://localhost/path/to/file.txt"
);
}
#[test]
fn test_strip_prefix_different_schemes() {
let base = Uri::parse("http://path/").unwrap();
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(
uri.strip_prefix(&base).as_str(),
"file://localhost/path/to/file.txt"
);
}
#[test]
fn test_strip_prefix_base_longer_than_uri() {
let base = Uri::parse("file://localhost/path/to/very/long/").unwrap();
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(
uri.strip_prefix(&base).as_str(),
"file://localhost/path/to/file.txt"
);
}
#[test]
fn test_to_scoped_string() {
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert_eq!(uri.to_scoped_string(), "path::to::file.txt");
}
#[test]
fn test_to_scoped_string_with_host() {
let uri = Uri::parse("file://example.com/path/to/file.txt").unwrap();
assert_eq!(uri.to_scoped_string(), "path::to::file.txt");
}
#[test]
fn test_to_scoped_string_with_empty_host() {
let uri = Uri::parse("file:///path/to/file.txt").unwrap();
assert_eq!(uri.to_scoped_string(), "path::to::file.txt");
}
#[test]
fn test_has_extension() {
let uri = Uri::parse("file://localhost/path/to/file.txt").unwrap();
assert!(uri.has_extension(".txt"));
assert!(!uri.has_extension(".rs"));
}
#[test]
fn test_to_scoped_string_consecutive_slashes() {
let uri = Uri::parse("file://localhost/path//to///file.txt").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "path::to::file.txt");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_empty_segments() {
let uri = Uri::parse("file://localhost/path/to/dir/").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "path::to::dir");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_root_only() {
let uri = Uri::parse("file:///").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_single_segment() {
let uri = Uri::parse("file://example/single").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "single");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_host_only() {
let uri = Uri::parse("file://example").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_whitespace_segments() {
let uri = Uri::parse("file:///path/%20/to/file.txt").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "path::%20::to::file.txt");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_multiple_empty_segments() {
let uri = Uri::parse("file:///path///to////file.txt").unwrap();
let scoped = uri.to_scoped_string();
assert_eq!(scoped, "path::to::file.txt");
assert!(!scoped.contains(":::"));
}
#[test]
fn test_to_scoped_string_no_consecutive_colons() {
let test_cases = vec![
"file:///",
"file:///path",
"file:///path/",
"file:///path//",
"file:///path///",
"file:///path/to/file.txt",
"file:///path//to///file.txt",
"file://host/path/to/file.txt",
"file://host//path///to////file.txt",
];
for case in test_cases {
let uri = Uri::parse(case).unwrap();
let scoped = uri.to_scoped_string();
assert!(
!scoped.contains(":::"),
"Uri '{case}' produced scoped string '{scoped}' with consecutive colons",
);
}
}
#[test]
fn test_to_scoped_string_with_query() {
let uri =
Uri::parse("file://localhost/path/to/file.txt?param=value").unwrap();
assert_eq!(uri.to_scoped_string(), "path::to::file.txt?param=value");
}
#[test]
fn test_to_scoped_string_with_multiple_query_params() {
let uri = Uri::parse(
"file://localhost/path/to/file.txt?param1=value1¶m2=value2",
)
.unwrap();
assert_eq!(
uri.to_scoped_string(),
"path::to::file.txt?param1=value1¶m2=value2"
);
}
#[test]
fn test_to_scoped_string_with_query_no_path() {
let uri = Uri::parse("file://localhost?param=value").unwrap();
assert_eq!(uri.to_scoped_string(), "?param=value");
}
#[test]
fn test_to_scoped_string_with_query_root_path() {
let uri = Uri::parse("file:////?param=value").unwrap();
assert_eq!(uri.to_scoped_string(), "?param=value");
}
#[test]
fn test_to_scoped_string_with_empty_query() {
let uri = Uri::parse("file://localhost/path/to/file.txt?").unwrap();
assert_eq!(uri.to_scoped_string(), "path::to::file.txt?");
}
#[test]
fn test_starts_with_file_under_base() {
let base = Uri::parse("file:///project/src/").unwrap();
let uri = Uri::parse("file:///project/src/utils/helpers.bl").unwrap();
assert!(uri.starts_with(&base));
}
#[test]
fn test_starts_with_same_uri() {
let uri = Uri::parse("file:///project/src/").unwrap();
assert!(uri.starts_with(&uri));
}
#[test]
fn test_starts_with_different_path() {
let base = Uri::parse("file:///project/src/").unwrap();
let uri = Uri::parse("file:///other/path/file.bl").unwrap();
assert!(!uri.starts_with(&base));
}
#[test]
fn test_starts_with_different_scheme() {
let base = Uri::parse("https:///project/src/").unwrap();
let uri = Uri::parse("file:///project/src/file.bl").unwrap();
assert!(!uri.starts_with(&base));
}
#[test]
fn test_starts_with_different_authority() {
let base = Uri::parse("file://other/project/src/").unwrap();
let uri = Uri::parse("file://localhost/project/src/file.bl").unwrap();
assert!(!uri.starts_with(&base));
}
#[test]
fn test_starts_with_partial_segment_no_match() {
let base = Uri::parse("file:///project/src").unwrap();
let uri = Uri::parse("file:///project/srclib/file.bl").unwrap();
assert!(uri.starts_with(&base));
}
#[test]
fn test_starts_with_trailing_slash_prevents_partial() {
let base = Uri::parse("file:///project/src/").unwrap();
let uri = Uri::parse("file:///project/srclib/file.bl").unwrap();
assert!(!uri.starts_with(&base));
}
#[test]
fn test_starts_with_deeply_nested() {
let base = Uri::parse("file:///project/").unwrap();
let uri = Uri::parse("file:///project/a/b/c/d/e.bl").unwrap();
assert!(uri.starts_with(&base));
}
#[test]
fn test_starts_with_base_longer_than_uri() {
let base = Uri::parse("file:///project/src/deep/nested/").unwrap();
let uri = Uri::parse("file:///project/src/").unwrap();
assert!(!uri.starts_with(&base));
}
}