Skip to main content

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