ferrous_actions/node/
path.rs1use js_sys::JsString;
2use std::borrow::Cow;
3use std::sync::LazyLock;
4use wasm_bindgen::JsCast as _;
5
6#[derive(Clone)]
8pub struct Path {
9 inner: JsString,
10}
11
12static SEPARATOR: LazyLock<String> =
13 LazyLock::new(|| ffi::SEPARATOR.with(|v| v.dyn_ref::<JsString>().expect("separator wasn't a string").into()));
14
15static DELIMITER: LazyLock<String> =
16 LazyLock::new(|| ffi::DELIMITER.with(|v| v.dyn_ref::<JsString>().expect("delimiter wasn't a string").into()));
17
18impl std::fmt::Display for Path {
19 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
20 let string = String::from(&self.inner);
21 string.fmt(formatter)
22 }
23}
24
25impl PartialEq for Path {
26 fn eq(&self, rhs: &Path) -> bool {
27 if self.is_absolute() == rhs.is_absolute() {
30 let relative = ffi::relative(&self.inner, &rhs.inner);
32 relative.length() == 0
33 } else {
34 false
35 }
36 }
37}
38
39impl Path {
40 pub fn push<P: Into<Path>>(&mut self, path: P) {
44 let path = path.into();
45 let joined = if path.is_absolute() {
46 path.inner
47 } else {
48 ffi::join(vec![self.inner.clone(), path.inner])
49 };
50 self.inner = joined;
51 }
52
53 pub fn to_js_string(&self) -> JsString {
55 self.inner.clone()
56 }
57
58 #[must_use]
60 pub fn parent(&self) -> Path {
61 let parent = ffi::dirname(&self.inner);
62 Path { inner: parent }
63 }
64
65 pub fn is_absolute(&self) -> bool {
67 ffi::is_absolute(&self.inner)
68 }
69
70 pub fn file_name(&self) -> String {
72 let result = ffi::basename(&self.inner, None);
73 result.into()
74 }
75
76 pub async fn exists(&self) -> bool {
78 super::fs::ffi::access(&self.inner, None).await.is_ok()
79 }
80
81 #[must_use]
83 pub fn join<P: Into<Path>>(&self, path: P) -> Path {
84 let mut result = self.clone();
85 result.push(path.into());
86 result
87 }
88
89 #[must_use]
91 pub fn relative_to<P: Into<Path>>(&self, path: P) -> Path {
92 let path = path.into();
93 let relative = ffi::relative(&path.inner, &self.inner);
94 if relative.length() == 0 {
95 ".".into()
96 } else {
97 relative.into()
98 }
99 }
100}
101
102impl std::fmt::Debug for Path {
103 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
104 write!(formatter, "{}", self)
105 }
106}
107
108impl From<&JsString> for Path {
109 fn from(path: &JsString) -> Path {
110 let path = ffi::normalize(path);
111 Path { inner: path }
112 }
113}
114
115impl From<JsString> for Path {
116 fn from(path: JsString) -> Path {
117 Path::from(&path)
118 }
119}
120
121impl From<&Path> for Path {
122 fn from(path: &Path) -> Path {
123 path.clone()
124 }
125}
126
127impl From<&str> for Path {
128 fn from(path: &str) -> Path {
129 let path: JsString = path.into();
130 let path = ffi::normalize(&path);
131 Path { inner: path }
132 }
133}
134
135impl From<&String> for Path {
136 fn from(path: &String) -> Path {
137 Path::from(path.as_str())
138 }
139}
140
141impl From<Path> for JsString {
142 fn from(path: Path) -> JsString {
143 path.inner
144 }
145}
146
147impl From<&Path> for JsString {
148 fn from(path: &Path) -> JsString {
149 path.inner.clone()
150 }
151}
152
153pub fn delimiter() -> Cow<'static, str> {
155 DELIMITER.as_str().into()
156}
157
158pub fn separator() -> Cow<'static, str> {
160 SEPARATOR.as_str().into()
161}
162
163pub mod ffi {
165 use js_sys::{JsString, Object};
166 use wasm_bindgen::prelude::*;
167
168 #[wasm_bindgen(module = "path")]
169 extern "C" {
170 #[wasm_bindgen(thread_local_v2, js_name = "delimiter")]
171 pub static DELIMITER: Object;
172
173 #[wasm_bindgen(thread_local_v2, js_name = "sep")]
174 pub static SEPARATOR: Object;
175
176 pub fn normalize(path: &JsString) -> JsString;
177 #[wasm_bindgen(variadic)]
178 pub fn join(paths: Vec<JsString>) -> JsString;
179 #[wasm_bindgen(variadic)]
180 pub fn resolve(paths: Vec<JsString>) -> JsString;
181 #[wasm_bindgen]
182 pub fn dirname(path: &JsString) -> JsString;
183 #[wasm_bindgen(js_name = "isAbsolute")]
184 pub fn is_absolute(path: &JsString) -> bool;
185 #[wasm_bindgen]
186 pub fn relative(from: &JsString, to: &JsString) -> JsString;
187 #[wasm_bindgen]
188 pub fn basename(path: &JsString, suffix: Option<JsString>) -> JsString;
189 }
190}
191
192#[cfg(test)]
193mod test {
194 use super::Path;
195 use crate::node;
196 use wasm_bindgen::JsValue;
197 use wasm_bindgen_test::wasm_bindgen_test;
198
199 #[wasm_bindgen_test]
200 fn check_absolute() {
201 let cwd = node::process::cwd();
202 assert!(cwd.is_absolute());
203 }
204
205 #[wasm_bindgen_test]
206 fn check_relative() {
207 let relative = Path::from(&format!("{}{}{}", "a", super::separator(), "b"));
208 assert!(!relative.is_absolute());
209 }
210
211 #[wasm_bindgen_test]
212 fn check_separator() {
213 let separator = super::separator();
214 assert!(separator == "/" || separator == "\\");
215 }
216
217 #[wasm_bindgen_test]
218 fn check_delimiter() {
219 let delimiter = super::delimiter();
220 assert!(delimiter == ";" || delimiter == ":");
221 }
222
223 #[wasm_bindgen_test]
224 fn check_parent() {
225 let parent_name = "parent";
226 let path = Path::from(&format!("{}{}{}", parent_name, super::separator(), "child"));
227 let parent_path = path.parent();
228 assert_eq!(parent_path.to_string(), parent_name);
229 }
230
231 #[wasm_bindgen_test]
232 fn check_basename() {
233 let child_base = "child.";
234 let child_ext = ".extension";
235 let child_name = format!("{}{}", child_base, child_ext);
236 let path = Path::from(&format!("{}{}{}", "parent", super::separator(), child_name));
237 assert_eq!(child_name, path.file_name());
238 assert_eq!(
239 child_name,
240 String::from(super::ffi::basename(&path.to_js_string(), None))
241 );
242 assert_eq!(
243 child_name,
244 String::from(super::ffi::basename(&path.to_js_string(), Some(".nomatch".into())))
245 );
246 assert_eq!(
247 child_base,
248 String::from(super::ffi::basename(&path.to_js_string(), Some(child_ext.into())))
249 );
250 }
251
252 #[wasm_bindgen_test]
253 fn check_push() {
254 let parent_name = "a";
255 let child_name = "b";
256 let path_string = format!("{}{}{}", parent_name, super::separator(), child_name);
257 let mut path = Path::from(parent_name);
258 path.push(child_name);
259 assert_eq!(path.to_string(), path_string);
260 }
261
262 #[wasm_bindgen_test]
263 fn check_join() {
264 let parent_name = "a";
265 let child_name = "b";
266 let path_string = format!("{}{}{}", parent_name, super::separator(), child_name);
267 let path = Path::from(parent_name).join(child_name);
268 assert_eq!(path.to_string(), path_string);
269 }
270
271 #[wasm_bindgen_test]
272 fn check_current_normalization() {
273 use itertools::Itertools as _;
274 let current = ".";
275 let long_current = std::iter::repeat(current).take(10).join(&super::separator());
276 assert_eq!(Path::from(&long_current).to_string(), current);
277 }
278
279 #[wasm_bindgen_test]
280 fn check_parent_normalization() {
281 use itertools::Itertools as _;
282 let parent = "..";
283 let current = ".";
284 let count = 10;
285
286 let long_current = std::iter::repeat("child")
287 .take(count)
288 .chain(std::iter::repeat(parent).take(count))
289 .join(&super::separator());
290 assert_eq!(Path::from(&long_current).to_string(), current);
291
292 let long_parent = std::iter::repeat("child")
293 .take(count)
294 .chain(std::iter::repeat(parent).take(count + 1))
295 .join(&super::separator());
296 assert_eq!(Path::from(&long_parent).to_string(), parent);
297 }
298
299 #[wasm_bindgen_test]
300 async fn check_exists() -> Result<(), JsValue> {
301 let temp = node::os::temp_dir();
302 let file_name = format!("ferrous-actions-exists-test - {}", chrono::Local::now());
303 let temp_file_path = temp.join(&file_name);
304 let data = "Nothing to see here\n";
305 node::fs::write_file(&temp_file_path, data.as_bytes()).await?;
306 assert!(temp_file_path.exists().await);
307 node::fs::remove_file(&temp_file_path).await?;
308 assert!(!temp_file_path.exists().await);
309 Ok(())
310 }
311
312 #[wasm_bindgen_test]
313 fn check_equality() {
314 use itertools::Itertools as _;
315
316 assert_eq!(Path::from("a"), Path::from("a"));
320 assert_eq!(Path::from("."), Path::from("."));
321 assert_eq!(Path::from(".."), Path::from(".."));
322 assert_eq!(
323 Path::from(&format!("a{}..", super::separator())),
324 Path::from(&format!("b{}..", super::separator()))
325 );
326 assert_ne!(Path::from("."), Path::from(".."));
327 assert_ne!(Path::from("a"), Path::from("b"));
328
329 let path = ["a", "b", "c", "d"].into_iter().join(&super::separator());
330 assert_eq!(Path::from(&path), Path::from(&path));
331 }
332}