Skip to main content

jpx_core/extensions/
path.rs

1//! File path manipulation functions.
2
3use std::collections::HashSet;
4
5use serde_json::Value;
6
7use crate::functions::Function;
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12/// Register path functions filtered by the enabled set.
13pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
14    register_if_enabled(
15        runtime,
16        "path_basename",
17        enabled,
18        Box::new(PathBasenameFn::new()),
19    );
20    register_if_enabled(
21        runtime,
22        "path_dirname",
23        enabled,
24        Box::new(PathDirnameFn::new()),
25    );
26    register_if_enabled(runtime, "path_ext", enabled, Box::new(PathExtFn::new()));
27    register_if_enabled(
28        runtime,
29        "path_is_absolute",
30        enabled,
31        Box::new(PathIsAbsoluteFn::new()),
32    );
33    register_if_enabled(
34        runtime,
35        "path_is_relative",
36        enabled,
37        Box::new(PathIsRelativeFn::new()),
38    );
39    register_if_enabled(runtime, "path_join", enabled, Box::new(PathJoinFn::new()));
40    register_if_enabled(runtime, "path_stem", enabled, Box::new(PathStemFn::new()));
41}
42
43// =============================================================================
44// path_basename(string) -> string
45// =============================================================================
46
47defn!(PathBasenameFn, vec![arg!(string)], None);
48
49impl Function for PathBasenameFn {
50    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
51        self.signature.validate(args, ctx)?;
52        let path = args[0].as_str().unwrap();
53        let basename = std::path::Path::new(path)
54            .file_name()
55            .and_then(|s| s.to_str())
56            .unwrap_or("");
57        Ok(Value::String(basename.to_string()))
58    }
59}
60
61// =============================================================================
62// path_dirname(string) -> string
63// =============================================================================
64
65defn!(PathDirnameFn, vec![arg!(string)], None);
66
67impl Function for PathDirnameFn {
68    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
69        self.signature.validate(args, ctx)?;
70        let path = args[0].as_str().unwrap();
71        let dirname = std::path::Path::new(path)
72            .parent()
73            .and_then(|s| s.to_str())
74            .unwrap_or("");
75        Ok(Value::String(dirname.to_string()))
76    }
77}
78
79// =============================================================================
80// path_ext(string) -> string
81// =============================================================================
82
83defn!(PathExtFn, vec![arg!(string)], None);
84
85impl Function for PathExtFn {
86    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
87        self.signature.validate(args, ctx)?;
88        let path = args[0].as_str().unwrap();
89        let ext = std::path::Path::new(path)
90            .extension()
91            .and_then(|s| s.to_str())
92            .map(|s| format!(".{}", s))
93            .unwrap_or_default();
94        Ok(Value::String(ext))
95    }
96}
97
98// =============================================================================
99// path_is_absolute(string) -> boolean
100// =============================================================================
101
102defn!(PathIsAbsoluteFn, vec![arg!(string)], None);
103
104impl Function for PathIsAbsoluteFn {
105    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
106        self.signature.validate(args, ctx)?;
107        let path = args[0].as_str().unwrap();
108        Ok(Value::Bool(std::path::Path::new(path).is_absolute()))
109    }
110}
111
112// =============================================================================
113// path_is_relative(string) -> boolean
114// =============================================================================
115
116defn!(PathIsRelativeFn, vec![arg!(string)], None);
117
118impl Function for PathIsRelativeFn {
119    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
120        self.signature.validate(args, ctx)?;
121        let path = args[0].as_str().unwrap();
122        Ok(Value::Bool(std::path::Path::new(path).is_relative()))
123    }
124}
125
126// =============================================================================
127// path_join(array) -> string
128// =============================================================================
129
130defn!(PathJoinFn, vec![arg!(array)], None);
131
132impl Function for PathJoinFn {
133    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
134        self.signature.validate(args, ctx)?;
135        let arr = args[0].as_array().unwrap();
136        let mut path = std::path::PathBuf::new();
137        for part in arr {
138            if let Some(s) = part.as_str() {
139                path.push(s);
140            }
141        }
142        let result = path.to_str().unwrap_or("").to_string();
143        Ok(Value::String(result))
144    }
145}
146
147// =============================================================================
148// path_stem(string) -> string | null
149// =============================================================================
150
151defn!(PathStemFn, vec![arg!(string)], None);
152
153impl Function for PathStemFn {
154    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
155        self.signature.validate(args, ctx)?;
156        let path = args[0].as_str().unwrap();
157        let stem = std::path::Path::new(path)
158            .file_stem()
159            .and_then(|s| s.to_str());
160        match stem {
161            Some(s) => Ok(Value::String(s.to_string())),
162            None => Ok(Value::Null),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use crate::Runtime;
170    use serde_json::json;
171
172    fn setup_runtime() -> Runtime {
173        Runtime::builder()
174            .with_standard()
175            .with_all_extensions()
176            .build()
177    }
178
179    #[test]
180    fn test_path_basename() {
181        let runtime = setup_runtime();
182        let expr = runtime.compile("path_basename(@)").unwrap();
183        let data = json!("/path/to/file.txt");
184        let result = expr.search(&data).unwrap();
185        assert_eq!(result.as_str().unwrap(), "file.txt");
186    }
187
188    #[test]
189    fn test_path_dirname() {
190        let runtime = setup_runtime();
191        let expr = runtime.compile("path_dirname(@)").unwrap();
192        let data = json!("/path/to/file.txt");
193        let result = expr.search(&data).unwrap();
194        assert_eq!(result.as_str().unwrap(), "/path/to");
195    }
196
197    #[test]
198    fn test_path_ext() {
199        let runtime = setup_runtime();
200        let expr = runtime.compile("path_ext(@)").unwrap();
201        let data = json!("/path/to/file.txt");
202        let result = expr.search(&data).unwrap();
203        assert_eq!(result.as_str().unwrap(), ".txt");
204    }
205
206    #[test]
207    fn test_path_is_absolute() {
208        let runtime = setup_runtime();
209        let expr = runtime.compile("path_is_absolute(@)").unwrap();
210
211        assert_eq!(expr.search(&json!("/foo/bar")).unwrap(), json!(true));
212        assert_eq!(expr.search(&json!("foo/bar")).unwrap(), json!(false));
213        assert_eq!(expr.search(&json!("file.txt")).unwrap(), json!(false));
214    }
215
216    #[test]
217    fn test_path_is_relative() {
218        let runtime = setup_runtime();
219        let expr = runtime.compile("path_is_relative(@)").unwrap();
220
221        assert_eq!(expr.search(&json!("foo/bar")).unwrap(), json!(true));
222        assert_eq!(expr.search(&json!("file.txt")).unwrap(), json!(true));
223        assert_eq!(expr.search(&json!("/foo/bar")).unwrap(), json!(false));
224    }
225
226    #[test]
227    fn test_path_stem() {
228        let runtime = setup_runtime();
229        let expr = runtime.compile("path_stem(@)").unwrap();
230
231        assert_eq!(expr.search(&json!("file.txt")).unwrap(), json!("file"));
232        assert_eq!(
233            expr.search(&json!("/foo/bar.tar.gz")).unwrap(),
234            json!("bar.tar")
235        );
236        assert_eq!(expr.search(&json!("noext")).unwrap(), json!("noext"));
237        assert_eq!(expr.search(&json!("/foo/bar/")).unwrap(), json!("bar"));
238    }
239
240    #[test]
241    fn test_path_basename_no_dir() {
242        let runtime = setup_runtime();
243        let expr = runtime.compile("path_basename(@)").unwrap();
244        assert_eq!(expr.search(&json!("file.txt")).unwrap(), json!("file.txt"));
245    }
246
247    #[test]
248    fn test_path_dirname_root() {
249        let runtime = setup_runtime();
250        let expr = runtime.compile("path_dirname(@)").unwrap();
251        // Root file has "/" as dirname
252        assert_eq!(expr.search(&json!("/file.txt")).unwrap(), json!("/"));
253    }
254
255    #[test]
256    fn test_path_ext_no_extension() {
257        let runtime = setup_runtime();
258        let expr = runtime.compile("path_ext(@)").unwrap();
259        assert_eq!(expr.search(&json!("noext")).unwrap(), json!(""));
260    }
261
262    #[test]
263    fn test_path_ext_double_extension() {
264        let runtime = setup_runtime();
265        let expr = runtime.compile("path_ext(@)").unwrap();
266        assert_eq!(expr.search(&json!("file.tar.gz")).unwrap(), json!(".gz"));
267    }
268
269    #[test]
270    fn test_path_join() {
271        let runtime = setup_runtime();
272        let data = json!(["/usr", "local", "bin"]);
273        let expr = runtime.compile("path_join(@)").unwrap();
274        let result = expr.search(&data).unwrap();
275        assert_eq!(result.as_str().unwrap(), "/usr/local/bin");
276    }
277
278    #[test]
279    fn test_path_join_empty_array() {
280        let runtime = setup_runtime();
281        let data = json!([]);
282        let expr = runtime.compile("path_join(@)").unwrap();
283        let result = expr.search(&data).unwrap();
284        assert_eq!(result.as_str().unwrap(), "");
285    }
286
287    #[test]
288    fn test_path_stem_no_extension() {
289        let runtime = setup_runtime();
290        let expr = runtime.compile("path_stem(@)").unwrap();
291        assert_eq!(expr.search(&json!("Makefile")).unwrap(), json!("Makefile"));
292    }
293
294    #[test]
295    fn test_path_stem_dotfile() {
296        let runtime = setup_runtime();
297        let expr = runtime.compile("path_stem(@)").unwrap();
298        assert_eq!(
299            expr.search(&json!("/home/user/.bashrc")).unwrap(),
300            json!(".bashrc")
301        );
302    }
303}