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 extension(&self) -> Option<&str> {
61        #[cfg(target_arch = "wasm32")]
62        {
63            self.0
64                .extension()
65                .map(|s| std::str::from_utf8(s).map(|s| s.trim_start_matches('.')).unwrap_or(""))
66                .filter(|s| !s.is_empty())
67        }
68        #[cfg(not(target_arch = "wasm32"))]
69        {
70            self.0.extension().and_then(|s| s.to_str())
71        }
72    }
73
74    pub fn join(&self, path: &str) -> Self {
75        #[cfg(target_arch = "wasm32")]
76        {
77            TypedPath(self.0.join(path))
78        }
79        #[cfg(not(target_arch = "wasm32"))]
80        {
81            TypedPath(self.0.join(path))
82        }
83    }
84
85    pub fn join_typed(&self, path: &TypedPath) -> Self {
86        #[cfg(target_arch = "wasm32")]
87        {
88            TypedPath(self.0.join(path.0.to_path()))
89        }
90        #[cfg(not(target_arch = "wasm32"))]
91        {
92            TypedPath(self.0.join(&path.0))
93        }
94    }
95
96    pub fn parent(&self) -> Option<Self> {
97        #[cfg(target_arch = "wasm32")]
98        {
99            self.0.parent().map(|p| TypedPath(p.to_path_buf()))
100        }
101        #[cfg(not(target_arch = "wasm32"))]
102        {
103            self.0.parent().map(|p| TypedPath(p.to_path_buf()))
104        }
105    }
106
107    #[cfg(not(target_arch = "wasm32"))]
108    pub fn strip_prefix(&self, base: impl AsRef<std::path::Path>) -> Result<Self, std::path::StripPrefixError> {
109        self.0.strip_prefix(base).map(|p| TypedPath(p.to_path_buf()))
110    }
111
112    #[cfg(not(target_arch = "wasm32"))]
113    pub fn canonicalize(&self) -> Result<Self, std::io::Error> {
114        self.0.canonicalize().map(|p| TypedPath(p.to_path_buf()))
115    }
116
117    pub fn to_string_lossy(&self) -> String {
118        #[cfg(target_arch = "wasm32")]
119        {
120            self.0.to_path().to_string_lossy().to_string()
121        }
122        #[cfg(not(target_arch = "wasm32"))]
123        {
124            self.0.to_string_lossy().to_string()
125        }
126    }
127
128    pub fn display(&self) -> String {
129        #[cfg(target_arch = "wasm32")]
130        {
131            self.0.to_path().display().to_string()
132        }
133        #[cfg(not(target_arch = "wasm32"))]
134        {
135            self.0.display().to_string()
136        }
137    }
138
139    pub fn file_name(&self) -> Option<String> {
140        #[cfg(target_arch = "wasm32")]
141        {
142            self.0
143                .file_name()
144                .map(|s| std::str::from_utf8(s).unwrap_or(""))
145                .filter(|s| !s.is_empty())
146                .map(|s| s.to_string())
147        }
148        #[cfg(not(target_arch = "wasm32"))]
149        {
150            self.0.file_name().and_then(|s| s.to_str()).map(|s| s.to_string())
151        }
152    }
153}
154
155impl serde::Serialize for TypedPath {
156    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: serde::Serializer,
159    {
160        #[cfg(target_arch = "wasm32")]
161        {
162            self.0.to_str().serialize(serializer)
163        }
164        #[cfg(not(target_arch = "wasm32"))]
165        {
166            self.0.serialize(serializer)
167        }
168    }
169}
170
171impl<'de> serde::de::Deserialize<'de> for TypedPath {
172    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
173    where
174        D: serde::Deserializer<'de>,
175    {
176        #[cfg(target_arch = "wasm32")]
177        {
178            let path: String = serde::Deserialize::deserialize(deserializer)?;
179            Ok(TypedPath(typed_path::TypedPath::derive(&path).to_path_buf()))
180        }
181        #[cfg(not(target_arch = "wasm32"))]
182        {
183            let path: std::path::PathBuf = serde::Deserialize::deserialize(deserializer)?;
184            Ok(TypedPath(path))
185        }
186    }
187}
188
189impl ts_rs::TS for TypedPath {
190    type WithoutGenerics = Self;
191    type OptionInnerType = Self;
192
193    fn name() -> String {
194        "string".to_string()
195    }
196
197    fn decl() -> String {
198        std::path::PathBuf::decl()
199    }
200
201    fn decl_concrete() -> String {
202        std::path::PathBuf::decl_concrete()
203    }
204
205    fn inline() -> String {
206        std::path::PathBuf::inline()
207    }
208
209    fn inline_flattened() -> String {
210        std::path::PathBuf::inline_flattened()
211    }
212
213    fn output_path() -> Option<std::path::PathBuf> {
214        std::path::PathBuf::output_path()
215    }
216}
217
218impl schemars::JsonSchema for TypedPath {
219    fn schema_name() -> String {
220        "TypedPath".to_owned()
221    }
222
223    fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
224        // TODO: Actually generate a reasonable schema.
225        r#gen.subschema_for::<std::path::PathBuf>()
226    }
227}
228
229/// Turn `nested\foo\bar\main.kcl` or `nested/foo/bar/main.kcl`
230/// into a PathBuf that works on the host OS.
231///
232/// * Does **not** touch `..` or symlinks – call `canonicalize()` if you need that.
233/// * Returns an owned `PathBuf` only when normalisation was required.
234#[cfg(not(target_arch = "wasm32"))]
235fn normalise_import<S: AsRef<str>>(raw: S) -> std::path::PathBuf {
236    let s = raw.as_ref();
237    // On Unix we need to swap `\` → `/`.  On Windows we leave it alone.
238    // (Windows happily consumes `/`)
239    if cfg!(unix) && s.contains('\\') {
240        std::path::PathBuf::from(s.replace('\\', "/"))
241    } else {
242        std::path::Path::new(s).to_path_buf()
243    }
244}