Skip to main content

rajac_base/
file_path.rs

1//! File path wrapper type for efficient path handling.
2
3use crate::shared_string::SharedString;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::path::{Path, PathBuf};
6
7/// A wrapper around SharedString for file path handling.
8///
9/// This type provides efficient string-based path storage with cheap cloning,
10/// making it ideal for compiler internal path representation.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
12pub struct FilePath(pub SharedString);
13
14impl FilePath {
15    /// Creates a new FilePath from the given path string.
16    pub fn new<P: AsRef<std::path::Path>>(path: P) -> Self {
17        Self(SharedString::new(path.as_ref().to_string_lossy()))
18    }
19
20    /// Creates a new FilePath from a string.
21    pub fn from_string(s: impl Into<String>) -> Self {
22        Self(SharedString::new(s.into()))
23    }
24
25    /// Returns the path as a string.
26    pub fn as_str(&self) -> &str {
27        self.0.as_str()
28    }
29
30    /// Returns the path as a Path.
31    pub fn as_path(&self) -> &Path {
32        Path::new(self.0.as_str())
33    }
34
35    /// Returns the underlying SharedString.
36    pub fn into_shared_string(self) -> SharedString {
37        self.0
38    }
39
40    /// Joins this path with another path component.
41    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    /// Returns the parent directory of this path, if any.
48    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    /// Returns the file name of this path, if any.
55    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    /// Returns the file stem (name without extension) of this path, if any.
62    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    /// Returns the extension of this path, if any.
69    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    /// Returns true if this path is absolute.
76    pub fn is_absolute(&self) -> bool {
77        Path::new(self.0.as_str()).is_absolute()
78    }
79
80    /// Returns true if this path is relative.
81    pub fn is_relative(&self) -> bool {
82        Path::new(self.0.as_str()).is_relative()
83    }
84
85    /// Normalizes the path by removing redundant components.
86    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                    // Remove the last normal component if there is one
94                    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                    // Skip current directory components
102                }
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        // Test writing
277        let buffer = original.write_to_vec().unwrap();
278        assert!(!buffer.is_empty());
279
280        // Test reading
281        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}