1use crate::shared_string::SharedString;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
12pub struct FilePath(pub SharedString);
13
14impl FilePath {
15 pub fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
17 Self(SharedString::new(path.as_ref().to_string_lossy()))
18 }
19
20 pub fn from_string(s: impl Into<String>) -> Self {
22 Self(SharedString::new(s.into()))
23 }
24
25 pub fn as_str(&self) -> &str {
27 self.0.as_str()
28 }
29
30 pub fn as_path(&self) -> &Path {
32 Path::new(self.0.as_str())
33 }
34
35 pub fn into_shared_string(self) -> SharedString {
37 self.0
38 }
39
40 pub fn join<P: AsRef<std::path::Path>>(&self, path: P) -> Self {
42 let mut path_buf = PathBuf::from(self.0.as_str());
43 path_buf.push(path);
44 Self(SharedString::new(path_buf.to_string_lossy()))
45 }
46
47 pub fn parent(&self) -> Option<Self> {
49 Path::new(self.0.as_str())
50 .parent()
51 .map(|p| Self(SharedString::new(p.to_string_lossy())))
52 }
53
54 pub fn file_name(&self) -> Option<&str> {
56 Path::new(self.0.as_str())
57 .file_name()
58 .and_then(|s| s.to_str())
59 }
60
61 pub fn file_stem(&self) -> Option<&str> {
63 Path::new(self.0.as_str())
64 .file_stem()
65 .and_then(|s| s.to_str())
66 }
67
68 pub fn extension(&self) -> Option<&str> {
70 Path::new(self.0.as_str())
71 .extension()
72 .and_then(|s| s.to_str())
73 }
74
75 pub fn is_absolute(&self) -> bool {
77 Path::new(self.0.as_str()).is_absolute()
78 }
79
80 pub fn is_relative(&self) -> bool {
82 Path::new(self.0.as_str()).is_relative()
83 }
84
85 pub fn normalize(&self) -> Self {
87 let path = Path::new(self.0.as_str());
88 let mut components = Vec::new();
89
90 for component in path.components() {
91 match component {
92 std::path::Component::ParentDir => {
93 if let Some(last) = components.last()
95 && matches!(last, std::path::Component::Normal(_))
96 {
97 components.pop();
98 }
99 }
100 std::path::Component::CurDir => {
101 }
103 _ => {
104 components.push(component);
105 }
106 }
107 }
108
109 let normalized: PathBuf = components.iter().collect();
110 Self(SharedString::new(normalized.to_string_lossy()))
111 }
112}
113
114impl From<String> for FilePath {
115 fn from(s: String) -> Self {
116 Self(SharedString::new(s))
117 }
118}
119
120impl From<&str> for FilePath {
121 fn from(s: &str) -> Self {
122 Self(SharedString::new(s))
123 }
124}
125
126impl From<SharedString> for FilePath {
127 fn from(s: SharedString) -> Self {
128 Self(s)
129 }
130}
131
132impl From<&FilePath> for FilePath {
133 fn from(path: &FilePath) -> Self {
134 path.clone()
135 }
136}
137
138impl AsRef<Path> for FilePath {
139 fn as_ref(&self) -> &Path {
140 Path::new(self.0.as_str())
141 }
142}
143
144impl std::fmt::Display for FilePath {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{}", self.0)
147 }
148}
149
150impl std::ops::Deref for FilePath {
151 type Target = SharedString;
152
153 fn deref(&self) -> &Self::Target {
154 &self.0
155 }
156}
157
158impl<'a, C> speedy::Readable<'a, C> for FilePath
159where
160 C: speedy::Context,
161{
162 fn read_from<R: speedy::Reader<'a, C>>(reader: &mut R) -> Result<Self, C::Error> {
163 let shared_string = SharedString::read_from(reader)?;
164 Ok(FilePath(shared_string))
165 }
166}
167
168impl<C> speedy::Writable<C> for FilePath
169where
170 C: speedy::Context,
171{
172 fn write_to<W>(&self, writer: &mut W) -> Result<(), C::Error>
173 where
174 W: speedy::Writer<C> + ?Sized,
175 {
176 self.0.write_to(writer)
177 }
178}
179
180impl Serialize for FilePath {
181 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182 where
183 S: Serializer,
184 {
185 serializer.serialize_str(self.as_str())
186 }
187}
188
189impl<'de> Deserialize<'de> for FilePath {
190 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
191 where
192 D: Deserializer<'de>,
193 {
194 let value = String::deserialize(deserializer)?;
195 Ok(value.into())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use speedy::{Readable, Writable};
203
204 #[test]
205 fn test_file_path_creation() {
206 let p1 = FilePath::new("src/main.rs");
207 let p2 = FilePath::from_string("test.holo");
208 let p3: FilePath = "lib/core.rs".into();
209
210 assert_eq!(p1.as_str(), "src/main.rs");
211 assert_eq!(p2.as_str(), "test.holo");
212 assert_eq!(p3.as_str(), "lib/core.rs");
213 }
214
215 #[test]
216 fn test_file_path_equality() {
217 let p1 = FilePath::new("src/main.rs");
218 let p2 = FilePath::new("src/main.rs");
219 let p3 = FilePath::new("src/lib.rs");
220
221 assert_eq!(p1, p2);
222 assert_ne!(p1, p3);
223 }
224
225 #[test]
226 fn test_file_path_clone() {
227 let p1 = FilePath::new("src/main.rs");
228 let p2 = p1.clone();
229
230 assert_eq!(p1, p2);
231 assert_eq!(p1.as_str(), p2.as_str());
232 }
233
234 #[test]
235 fn test_file_path_join() {
236 let base = FilePath::new("src");
237 let joined = base.join("main.rs");
238 assert_eq!(joined.as_str(), "src/main.rs");
239 }
240
241 #[test]
242 fn test_file_path_components() {
243 let path = FilePath::new("src/main.rs");
244
245 assert_eq!(path.file_name(), Some("main.rs"));
246 assert_eq!(path.file_stem(), Some("main"));
247 assert_eq!(path.extension(), Some("rs"));
248
249 let parent = path.parent().unwrap();
250 assert_eq!(parent.as_str(), "src");
251 }
252
253 #[test]
254 fn test_file_path_normalize() {
255 let path = FilePath::new("src/../src/main.rs");
256 let normalized = path.normalize();
257 assert_eq!(normalized.as_str(), "src/main.rs");
258 }
259
260 #[test]
261 fn test_file_path_display() {
262 let path = FilePath::new("src/main.rs");
263 assert_eq!(format!("{}", path), "src/main.rs");
264 }
265
266 #[test]
267 fn test_file_path_default() {
268 let path = FilePath::default();
269 assert!(path.as_str().is_empty());
270 }
271
272 #[test]
273 fn test_file_path_speedy_serialization() {
274 let original = FilePath::new("src/main.rs");
275
276 let buffer = original.write_to_vec().unwrap();
278 assert!(!buffer.is_empty());
279
280 let deserialized = FilePath::read_from_buffer(&buffer).unwrap();
282 assert_eq!(original, deserialized);
283 assert_eq!(deserialized.as_str(), "src/main.rs");
284 }
285
286 #[test]
287 fn test_file_path_speedy_empty_path() {
288 let original = FilePath::default();
289
290 let buffer = original.write_to_vec().unwrap();
291 let deserialized = FilePath::read_from_buffer(&buffer).unwrap();
292
293 assert_eq!(original, deserialized);
294 assert!(deserialized.as_str().is_empty());
295 }
296
297 #[test]
298 fn test_file_path_speedy_complex_path() {
299 let original = FilePath::new("src/components/ui/button.rs");
300
301 let buffer = original.write_to_vec().unwrap();
302 let deserialized = FilePath::read_from_buffer(&buffer).unwrap();
303
304 assert_eq!(original, deserialized);
305 assert_eq!(deserialized.as_str(), "src/components/ui/button.rs");
306 }
307
308 #[test]
309 fn test_file_path_speedy_special_characters() {
310 let original = FilePath::new("path-with_dashes/123_file.holo");
311
312 let buffer = original.write_to_vec().unwrap();
313 let deserialized = FilePath::read_from_buffer(&buffer).unwrap();
314
315 assert_eq!(original, deserialized);
316 assert_eq!(deserialized.as_str(), "path-with_dashes/123_file.holo");
317 }
318}