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