Skip to main content

ocelot_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 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}