1use core::fmt;
4
5use alloc::string::String;
6
7#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(transparent))]
15pub struct VdirPath(String);
16
17impl VdirPath {
18 pub fn new(s: impl Into<String>) -> Self {
20 Self(s.into())
21 }
22
23 pub fn as_str(&self) -> &str {
25 &self.0
26 }
27
28 pub fn into_string(self) -> String {
30 self.0
31 }
32
33 pub fn is_empty(&self) -> bool {
35 self.0.is_empty()
36 }
37
38 pub fn join(&self, segment: &str) -> Self {
44 let mut out = self.clone();
45 out.push(segment);
46 out
47 }
48
49 pub fn push(&mut self, segment: &str) {
52 if !self.0.is_empty() && !self.0.ends_with('/') {
53 self.0.push('/');
54 }
55 self.0.push_str(segment);
56 }
57
58 pub fn file_name(&self) -> Option<&str> {
60 match self.0.rsplit_once('/') {
61 Some((_, name)) if !name.is_empty() => Some(name),
62 None if !self.0.is_empty() => Some(&self.0),
63 _ => None,
64 }
65 }
66
67 pub fn parent(&self) -> Option<&str> {
69 self.0.rsplit_once('/').map(|(parent, _)| parent)
70 }
71
72 pub fn with_file_name(&self, name: &str) -> Self {
76 match self.parent() {
77 Some(parent) => Self::new(parent).join(name),
78 None => Self::new(name),
79 }
80 }
81
82 pub fn strip_prefix(&self, prefix: &Self) -> Option<&str> {
85 let rest = self.0.strip_prefix(prefix.as_str())?;
86 Some(rest.strip_prefix('/').unwrap_or(rest))
87 }
88
89 pub fn starts_with(&self, prefix: &Self) -> bool {
91 self.0.starts_with(prefix.as_str())
92 }
93
94 pub fn components(&self) -> impl Iterator<Item = &str> {
96 self.0.split('/').filter(|c| !c.is_empty())
97 }
98}
99
100impl fmt::Display for VdirPath {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 fmt::Display::fmt(&self.0, f)
103 }
104}
105
106impl From<String> for VdirPath {
107 fn from(s: String) -> Self {
108 Self(s)
109 }
110}
111
112impl From<&str> for VdirPath {
113 fn from(s: &str) -> Self {
114 Self(s.into())
115 }
116}
117
118impl AsRef<str> for VdirPath {
119 fn as_ref(&self) -> &str {
120 &self.0
121 }
122}
123
124#[cfg(feature = "client")]
125impl AsRef<std::path::Path> for VdirPath {
126 fn as_ref(&self) -> &std::path::Path {
127 std::path::Path::new(&self.0)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use alloc::vec::Vec;
134
135 use crate::path::VdirPath;
136
137 #[test]
138 fn join_inserts_separator() {
139 let p = VdirPath::new("a");
140 assert_eq!(p.join("b").as_str(), "a/b");
141 }
142
143 #[test]
144 fn join_on_empty_skips_separator() {
145 let p = VdirPath::default();
146 assert_eq!(p.join("a").as_str(), "a");
147 }
148
149 #[test]
150 fn join_normalises_trailing_separator() {
151 let p = VdirPath::new("a/");
152 assert_eq!(p.join("b").as_str(), "a/b");
153 }
154
155 #[test]
156 fn file_name_returns_last_segment() {
157 assert_eq!(VdirPath::new("a/b/c").file_name(), Some("c"));
158 assert_eq!(VdirPath::new("c").file_name(), Some("c"));
159 assert_eq!(VdirPath::default().file_name(), None);
160 assert_eq!(VdirPath::new("a/").file_name(), None);
161 }
162
163 #[test]
164 fn parent_returns_path_without_last_segment() {
165 assert_eq!(VdirPath::new("a/b/c").parent(), Some("a/b"));
166 assert_eq!(VdirPath::new("a").parent(), None);
167 }
168
169 #[test]
170 fn with_file_name_replaces_last_segment() {
171 let p = VdirPath::new("a/b/c");
172 assert_eq!(p.with_file_name("d").as_str(), "a/b/d");
173
174 let p = VdirPath::new("a");
175 assert_eq!(p.with_file_name("z").as_str(), "z");
176 }
177
178 #[test]
179 fn strip_prefix_removes_leading_separator() {
180 let p = VdirPath::new("root/sub/leaf");
181 let root = VdirPath::new("root");
182 assert_eq!(p.strip_prefix(&root), Some("sub/leaf"));
183 }
184
185 #[test]
186 fn components_skips_empties() {
187 let p = VdirPath::new("/a//b/");
188 let parts: Vec<&str> = p.components().collect();
189 assert_eq!(parts, ["a", "b"]);
190 }
191}