Skip to main content

camel_api/
declarative.rs

1use crate::Value;
2use crate::error::CamelError;
3use crate::script_source;
4
5/// A language expression/predicate reference resolved by the runtime.
6#[derive(
7    Debug,
8    Clone,
9    PartialEq,
10    Eq,
11    serde::Serialize,
12    serde::Deserialize,
13    schemars::JsonSchema,
14    ts_rs::TS,
15)]
16#[serde(rename_all = "snake_case")]
17#[ts(rename_all = "snake_case")]
18pub struct LanguageExpressionDef {
19    pub language: String,
20    pub source: String,
21}
22
23impl LanguageExpressionDef {
24    /// Build a language expression from a script file, inferring the
25    /// language from the file extension.
26    ///
27    /// Relative paths follow standard library semantics and resolve against
28    /// the process current working directory. The inferred language must map
29    /// to a known script extension; use [`Self::file_with_language`] when the
30    /// extension is not enough.
31    pub fn file(path: impl AsRef<std::path::Path>) -> Result<Self, CamelError> {
32        let path = path.as_ref();
33        let language = script_source::infer_script_language(path)?;
34        let source = script_source::read_script_source(path)?;
35        Ok(Self { language, source })
36    }
37
38    /// Build a language expression from a script file, using the given
39    /// language key instead of inferring it from the extension.
40    ///
41    /// Relative paths follow standard library semantics and resolve against
42    /// the process current working directory.
43    pub fn file_with_language(
44        path: impl AsRef<std::path::Path>,
45        language: impl Into<String>,
46    ) -> Result<Self, CamelError> {
47        let source = script_source::read_script_source(path.as_ref())?;
48        Ok(Self {
49            language: language.into(),
50            source,
51        })
52    }
53}
54
55/// A declarative value source for set_header / set_body.
56#[derive(Debug, Clone, PartialEq)]
57#[non_exhaustive]
58pub enum ValueSourceDef {
59    Literal(Value),
60    Expression(LanguageExpressionDef),
61}
62
63#[cfg(test)]
64mod tests {
65    use std::fs;
66    use std::path::Path;
67
68    use tempfile::TempDir;
69
70    use super::LanguageExpressionDef;
71
72    #[test]
73    fn file_constructor_infers_language() {
74        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
75        let scripts = dir.path().join("scripts");
76        fs::create_dir(&scripts).expect("create scripts dir");
77        let path = scripts.join("x.rhai");
78        let contents = "let value = 1;\n";
79        fs::write(&path, contents).expect("write fixture");
80
81        let def = LanguageExpressionDef::file(&path).expect("build from script file");
82
83        assert_eq!(def.language, "rhai");
84        assert_eq!(def.source, contents);
85    }
86
87    #[test]
88    fn file_constructor_explicit_language_wins() {
89        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
90        let path = dir.path().join("notes.txt");
91        let contents = "hello\n";
92        fs::write(&path, contents).expect("write fixture");
93
94        let def = LanguageExpressionDef::file_with_language(&path, "simple")
95            .expect("build with explicit language");
96
97        assert_eq!(def.language, "simple");
98        assert_eq!(def.source, contents);
99    }
100
101    #[test]
102    fn file_constructor_missing_file_errors() {
103        let dir: TempDir = tempfile::tempdir().expect("create tempdir");
104        let path = dir.path().join("absent.rhai");
105
106        let error = LanguageExpressionDef::file(&path).expect_err("missing file must fail");
107        let message = error.to_string();
108
109        assert!(
110            message.contains(&path.display().to_string()),
111            "message: {message}"
112        );
113    }
114
115    #[test]
116    fn file_constructor_relative_missing_file_names_absolute_path() {
117        let cwd = std::env::current_dir().expect("read current dir");
118        let relative = Path::new("nonexistent_dir_xyz/absent.rhai");
119
120        let error = LanguageExpressionDef::file(relative).expect_err("missing file must fail");
121        let message = error.to_string();
122
123        let expected = cwd.join(relative);
124        assert!(expected.is_absolute());
125        assert!(
126            message.contains(&expected.display().to_string()),
127            "message: {message}"
128        );
129    }
130}