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 Serialize for FilePath {
159 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160 where
161 S: Serializer,
162 {
163 serializer.serialize_str(self.as_str())
164 }
165}
166
167impl<'de> Deserialize<'de> for FilePath {
168 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
169 where
170 D: Deserializer<'de>,
171 {
172 let value = String::deserialize(deserializer)?;
173 Ok(value.into())
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_file_path_creation() {
183 let p1 = FilePath::new("src/main.rs");
184 let p2 = FilePath::from_string("test.holo");
185 let p3: FilePath = "lib/core.rs".into();
186
187 assert_eq!(p1.as_str(), "src/main.rs");
188 assert_eq!(p2.as_str(), "test.holo");
189 assert_eq!(p3.as_str(), "lib/core.rs");
190 }
191
192 #[test]
193 fn test_file_path_equality() {
194 let p1 = FilePath::new("src/main.rs");
195 let p2 = FilePath::new("src/main.rs");
196 let p3 = FilePath::new("src/lib.rs");
197
198 assert_eq!(p1, p2);
199 assert_ne!(p1, p3);
200 }
201
202 #[test]
203 fn test_file_path_clone() {
204 let p1 = FilePath::new("src/main.rs");
205 let p2 = p1.clone();
206
207 assert_eq!(p1, p2);
208 assert_eq!(p1.as_str(), p2.as_str());
209 }
210
211 #[test]
212 fn test_file_path_join() {
213 let base = FilePath::new("src");
214 let joined = base.join("main.rs");
215 assert_eq!(joined.as_str(), "src/main.rs");
216 }
217
218 #[test]
219 fn test_file_path_components() {
220 let path = FilePath::new("src/main.rs");
221
222 assert_eq!(path.file_name(), Some("main.rs"));
223 assert_eq!(path.file_stem(), Some("main"));
224 assert_eq!(path.extension(), Some("rs"));
225
226 let parent = path.parent().unwrap();
227 assert_eq!(parent.as_str(), "src");
228 }
229
230 #[test]
231 fn test_file_path_normalize() {
232 let path = FilePath::new("src/../src/main.rs");
233 let normalized = path.normalize();
234 assert_eq!(normalized.as_str(), "src/main.rs");
235 }
236
237 #[test]
238 fn test_file_path_display() {
239 let path = FilePath::new("src/main.rs");
240 assert_eq!(format!("{}", path), "src/main.rs");
241 }
242
243 #[test]
244 fn test_file_path_default() {
245 let path = FilePath::default();
246 assert!(path.as_str().is_empty());
247 }
248}