Skip to main content

kcl_lib/execution/
typed_path.rs

1//! A typed path type so that in wasm we can track if its a windows or unix path.
2//! On non-wasm platforms, this is just a std::path::PathBuf.
3
4#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5pub struct TypedPath(
6    #[cfg(target_arch = "wasm32")] pub typed_path::TypedPathBuf,
7    #[cfg(not(target_arch = "wasm32"))] pub std::path::PathBuf,
8);
9
10impl std::fmt::Display for TypedPath {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        #[cfg(target_arch = "wasm32")]
13        {
14            self.0.to_path().display().fmt(f)
15        }
16        #[cfg(not(target_arch = "wasm32"))]
17        {
18            self.0.display().fmt(f)
19        }
20    }
21}
22
23impl Default for TypedPath {
24    fn default() -> Self {
25        #[cfg(target_arch = "wasm32")]
26        {
27            TypedPath(typed_path::TypedPath::derive("").to_path_buf())
28        }
29        #[cfg(not(target_arch = "wasm32"))]
30        {
31            TypedPath(std::path::PathBuf::new())
32        }
33    }
34}
35
36impl From<&String> for TypedPath {
37    fn from(path: &String) -> Self {
38        TypedPath::new(path)
39    }
40}
41
42impl From<&str> for TypedPath {
43    fn from(path: &str) -> Self {
44        TypedPath::new(path)
45    }
46}
47
48impl TypedPath {
49    pub fn new(path: &str) -> Self {
50        #[cfg(target_arch = "wasm32")]
51        {
52            TypedPath(typed_path::TypedPath::derive(path).to_path_buf())
53        }
54        #[cfg(not(target_arch = "wasm32"))]
55        {
56            TypedPath(normalise_import(path))
57        }
58    }
59
60    pub fn starts_with(&self, base: &TypedPath) -> bool {
61        #[cfg(target_arch = "wasm32")]
62        {
63            self.0.starts_with(base.0.as_ref())
64        }
65        #[cfg(not(target_arch = "wasm32"))]
66        {
67            self.0.starts_with(&base.0)
68        }
69    }
70
71    pub fn extension(&self) -> Option<&str> {
72        #[cfg(target_arch = "wasm32")]
73        {
74            self.0
75                .extension()
76                .map(|s| std::str::from_utf8(s).map(|s| s.trim_start_matches('.')).unwrap_or(""))
77                .filter(|s| !s.is_empty())
78        }
79        #[cfg(not(target_arch = "wasm32"))]
80        {
81            self.0.extension().and_then(|s| s.to_str())
82        }
83    }
84
85    pub fn is_absolute(&self) -> bool {
86        self.0.is_absolute()
87    }
88
89    pub fn join(&self, path: &str) -> Self {
90        #[cfg(target_arch = "wasm32")]
91        {
92            TypedPath(self.0.join(path))
93        }
94        #[cfg(not(target_arch = "wasm32"))]
95        {
96            TypedPath(self.0.join(path))
97        }
98    }
99
100    pub fn join_typed(&self, path: &TypedPath) -> Self {
101        #[cfg(target_arch = "wasm32")]
102        {
103            TypedPath(self.0.join(path.0.to_path()))
104        }
105        #[cfg(not(target_arch = "wasm32"))]
106        {
107            TypedPath(self.0.join(&path.0))
108        }
109    }
110
111    pub fn parent(&self) -> Option<Self> {
112        #[cfg(target_arch = "wasm32")]
113        {
114            self.0.parent().map(|p| TypedPath(p.to_path_buf()))
115        }
116        #[cfg(not(target_arch = "wasm32"))]
117        {
118            self.0.parent().map(|p| TypedPath(p.to_path_buf()))
119        }
120    }
121
122    #[cfg(not(target_arch = "wasm32"))]
123    pub fn strip_prefix(&self, base: impl AsRef<std::path::Path>) -> Result<Self, std::path::StripPrefixError> {
124        self.0.strip_prefix(base).map(|p| TypedPath(p.to_path_buf()))
125    }
126
127    #[cfg(not(target_arch = "wasm32"))]
128    pub fn canonicalize(&self) -> Result<Self, std::io::Error> {
129        self.0.canonicalize().map(TypedPath)
130    }
131
132    pub fn to_string_lossy(&self) -> String {
133        #[cfg(target_arch = "wasm32")]
134        {
135            self.0.to_path().to_string_lossy().to_string()
136        }
137        #[cfg(not(target_arch = "wasm32"))]
138        {
139            self.0.to_string_lossy().to_string()
140        }
141    }
142
143    pub fn display(&self) -> String {
144        #[cfg(target_arch = "wasm32")]
145        {
146            self.0.to_path().display().to_string()
147        }
148        #[cfg(not(target_arch = "wasm32"))]
149        {
150            self.0.display().to_string()
151        }
152    }
153
154    pub fn file_name(&self) -> Option<String> {
155        #[cfg(target_arch = "wasm32")]
156        {
157            self.0
158                .file_name()
159                .map(|s| std::str::from_utf8(s).unwrap_or(""))
160                .filter(|s| !s.is_empty())
161                .map(|s| s.to_string())
162        }
163        #[cfg(not(target_arch = "wasm32"))]
164        {
165            self.0.file_name().and_then(|s| s.to_str()).map(|s| s.to_string())
166        }
167    }
168}
169
170impl serde::Serialize for TypedPath {
171    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172    where
173        S: serde::Serializer,
174    {
175        #[cfg(target_arch = "wasm32")]
176        {
177            self.0.to_str().serialize(serializer)
178        }
179        #[cfg(not(target_arch = "wasm32"))]
180        {
181            self.0.serialize(serializer)
182        }
183    }
184}
185
186impl<'de> serde::de::Deserialize<'de> for TypedPath {
187    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
188    where
189        D: serde::Deserializer<'de>,
190    {
191        #[cfg(target_arch = "wasm32")]
192        {
193            let path: String = serde::Deserialize::deserialize(deserializer)?;
194            Ok(TypedPath(typed_path::TypedPath::derive(&path).to_path_buf()))
195        }
196        #[cfg(not(target_arch = "wasm32"))]
197        {
198            let path: std::path::PathBuf = serde::Deserialize::deserialize(deserializer)?;
199            Ok(TypedPath(path))
200        }
201    }
202}
203
204impl ts_rs::TS for TypedPath {
205    type WithoutGenerics = Self;
206    type OptionInnerType = Self;
207
208    fn name(_: &ts_rs::Config) -> String {
209        "string".to_string()
210    }
211
212    fn decl(config: &ts_rs::Config) -> String {
213        std::path::PathBuf::decl(config)
214    }
215
216    fn decl_concrete(config: &ts_rs::Config) -> String {
217        std::path::PathBuf::decl_concrete(config)
218    }
219
220    fn inline(config: &ts_rs::Config) -> String {
221        std::path::PathBuf::inline(config)
222    }
223
224    fn inline_flattened(config: &ts_rs::Config) -> String {
225        std::path::PathBuf::inline_flattened(config)
226    }
227
228    fn output_path() -> Option<std::path::PathBuf> {
229        std::path::PathBuf::output_path()
230    }
231}
232
233/// Turn `nested\foo\bar\main.kcl` or `nested/foo/bar/main.kcl`
234/// into a PathBuf that works on the host OS.
235///
236/// * Does **not** touch `..` or symlinks – call `canonicalize()` if you need that.
237/// * Returns an owned `PathBuf` only when normalisation was required.
238#[cfg(not(target_arch = "wasm32"))]
239fn normalise_import<S: AsRef<str>>(raw: S) -> std::path::PathBuf {
240    let s = raw.as_ref();
241    // On Unix we need to swap `\` → `/`.  On Windows we leave it alone.
242    // (Windows happily consumes `/`)
243    if cfg!(unix) && s.contains('\\') {
244        std::path::PathBuf::from(s.replace('\\', "/"))
245    } else {
246        std::path::Path::new(s).to_path_buf()
247    }
248}