Skip to main content

camel_api/
script_source.rs

1//! Helpers for loading language-expression sources from script files.
2
3use std::path::Path;
4use std::path::PathBuf;
5
6use crate::error::CamelError;
7
8/// Resolve `path` to an absolute path for diagnostics without touching the
9/// filesystem: absolute paths pass through, relative paths join the process
10/// current working directory.
11fn resolve_path(path: &Path) -> PathBuf {
12    if path.is_absolute() {
13        return path.to_path_buf();
14    }
15    match std::env::current_dir() {
16        Ok(cwd) => cwd.join(path),
17        Err(_) => path.to_path_buf(),
18    }
19}
20
21/// Infer the language registry key for a script file from its extension.
22///
23/// The extension match is case-sensitive. Relative paths are resolved
24/// against the process current working directory for diagnostics.
25pub fn infer_script_language(path: &Path) -> Result<String, CamelError> {
26    let resolved = resolve_path(path);
27    let Some(extension) = path.extension().and_then(std::ffi::OsStr::to_str) else {
28        return Err(CamelError::RouteError(format!(
29            "cannot infer script language for `{}`: missing file extension; set `language:` explicitly",
30            resolved.display()
31        )));
32    };
33    let language = match extension {
34        "rhai" => "rhai",
35        "js" => "js",
36        "jsonpath" => "jsonpath",
37        "xpath" => "xpath",
38        "jinja" => "minijinja",
39        "jinja2" => "minijinja",
40        _ => {
41            return Err(CamelError::RouteError(format!(
42                "cannot infer script language for `{}`: unknown extension `.{extension}`; set `language:` explicitly",
43                resolved.display()
44            )));
45        }
46    };
47    Ok(language.to_string())
48}
49
50/// Read the contents of a script file verbatim.
51///
52/// Relative paths are resolved against the process current working
53/// directory for diagnostics. Empty (or whitespace-only) files are
54/// rejected.
55pub fn read_script_source(path: &Path) -> Result<String, CamelError> {
56    let resolved = resolve_path(path);
57    let contents = std::fs::read_to_string(&resolved).map_err(|error| {
58        CamelError::RouteError(format!(
59            "cannot read script file `{}`: {error}",
60            resolved.display()
61        ))
62    })?;
63    if contents.trim().is_empty() {
64        return Err(CamelError::RouteError(format!(
65            "script file `{}` is empty",
66            resolved.display()
67        )));
68    }
69    Ok(contents)
70}
71
72#[cfg(test)]
73mod tests {
74    use std::fs;
75
76    use tempfile::TempDir;
77
78    use super::infer_script_language;
79    use super::read_script_source;
80
81    #[test]
82    fn infer_script_language_maps_known_extensions() {
83        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
84        let cases = [
85            ("a.rhai", "rhai"),
86            ("b.js", "js"),
87            ("c.jsonpath", "jsonpath"),
88            ("d.xpath", "xpath"),
89            ("e.jinja", "minijinja"),
90            ("f.jinja2", "minijinja"),
91        ];
92
93        for (name, expected) in cases {
94            let path = dir.path().join(name);
95            fs::write(&path, "x").expect("write fixture");
96
97            let language =
98                infer_script_language(&path).expect("infer language from known extension");
99
100            assert_eq!(language, expected, "language for {name}");
101        }
102    }
103
104    #[test]
105    fn infer_script_language_rejects_unknown_extension() {
106        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
107        let path = dir.path().join("x.xyz");
108        fs::write(&path, "x").expect("write fixture");
109
110        let error = infer_script_language(&path).expect_err("unknown extension must fail");
111        let message = error.to_string();
112
113        assert!(
114            message.contains(&path.display().to_string()),
115            "message: {message}"
116        );
117        assert!(message.contains("xyz"), "message: {message}");
118    }
119
120    #[test]
121    fn read_script_source_rejects_missing_file() {
122        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
123        let path = dir.path().join("absent.rhai");
124
125        let error = read_script_source(&path).expect_err("missing file must fail");
126        let message = error.to_string();
127
128        assert!(
129            message.contains(&path.display().to_string()),
130            "message: {message}"
131        );
132    }
133
134    #[test]
135    fn read_script_source_rejects_empty_file() {
136        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
137        let path = dir.path().join("blank.rhai");
138        fs::write(&path, " \n\t\n").expect("write fixture");
139
140        let error = read_script_source(&path).expect_err("blank file must fail");
141        let message = error.to_string();
142
143        assert!(message.contains("empty"), "message: {message}");
144        assert!(
145            message.contains(&path.display().to_string()),
146            "message: {message}"
147        );
148    }
149
150    #[test]
151    fn read_script_source_returns_verbatim_contents() {
152        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
153        let path = dir.path().join("verbatim.rhai");
154        let contents = "${env:SECRET}\n";
155        fs::write(&path, contents).expect("write fixture");
156
157        let read = read_script_source(&path).expect("read script source");
158
159        assert_eq!(read, contents);
160    }
161}