Skip to main content

camel_cli/template/
bean.rs

1use super::{TemplateFile, cargo_toml, gitignore, plugin_toml, to_pascal_case};
2
3pub fn bean_files(plugin_name: &str) -> Vec<TemplateFile> {
4    vec![
5        TemplateFile {
6            path: "Cargo.toml".to_string(),
7            content: cargo_toml(plugin_name),
8        },
9        TemplateFile {
10            path: "src/lib.rs".to_string(),
11            content: lib_rs(plugin_name),
12        },
13        TemplateFile {
14            path: "Camel.plugin.toml".to_string(),
15            content: plugin_toml(plugin_name, "bean"),
16        },
17        TemplateFile {
18            path: "README.md".to_string(),
19            content: bean_readme_md(plugin_name),
20        },
21        TemplateFile {
22            path: ".gitignore".to_string(),
23            content: gitignore().to_string(),
24        },
25        TemplateFile {
26            path: "wit/camel-bean.wit".to_string(),
27            content: camel_bean_wit().to_string(),
28        },
29        TemplateFile {
30            path: "wit/camel-plugin.wit".to_string(),
31            content: camel_plugin_wit().to_string(),
32        },
33    ]
34}
35
36fn lib_rs(plugin_name: &str) -> String {
37    let plugin_type = to_pascal_case(plugin_name);
38    format!(
39        "use bindings::camel::plugin::types::{{WasmBody, WasmError, WasmExchange}};\nuse bindings::Guest;\n\nmod bindings {{\n    wit_bindgen::generate!({{\n        world: \"bean\",\n        path: \"../wit\",\n    }});\n}}\n\nstruct {plugin_type};\n\nimpl Guest for {plugin_type} {{\n    fn init() -> Result<(), String> {{\n        Ok(())\n    }}\n\n    fn methods() -> Vec<String> {{\n        vec![\"hello\".into()]\n    }}\n\n    fn invoke(method: String, mut exchange: WasmExchange) -> Result<WasmExchange, WasmError> {{\n        match method.as_str() {{\n            \"hello\" => {{\n                let text = match &exchange.input.body {{\n                    WasmBody::Text(s) => s.clone(),\n                    _ => String::new(),\n                }};\n                exchange.input.body = WasmBody::Text(format!(\"Hello from {plugin_name}: {{text}}\"));\n                Ok(exchange)\n            }}\n            _ => Err(WasmError::ProcessorError(format!(\"unknown method: {{method}}\"))),\n        }}\n    }}\n}}\n\nbindings::export!({plugin_type} with_types_in bindings);\n"
40    )
41}
42
43fn camel_bean_wit() -> &'static str {
44    camel_wit::BEAN_WIT
45}
46
47fn camel_plugin_wit() -> &'static str {
48    camel_wit::PLUGIN_WIT
49}
50
51fn bean_readme_md(plugin_name: &str) -> String {
52    format!(
53        r#"# {plugin_name}
54
55WASM bean plugin for Camel.
56
57## Build
58
59```bash
60camel plugin build
61```
62
63## Use from `Camel.toml`
64
65```toml
66[default.beans.{plugin_name}]
67plugin = "{plugin_name}"
68
69# Optional runtime limits — defaults: 30s timeout, 50 MiB memory.
70[default.beans.{plugin_name}.limits]
71timeout-secs = 60
72max-memory = 104857600
73```
74
75## Files
76
77- `src/lib.rs`: bean entrypoint implementing `methods()` and `invoke(...)`
78- `wit/`: WIT definitions used for guest bindings generation
79- `Camel.plugin.toml`: plugin metadata for Camel
80"#
81    )
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn bean_files_contains_expected_paths() {
90        let files = bean_files("acme-bean");
91        let paths: Vec<&str> = files.iter().map(|f| f.path.as_str()).collect();
92
93        assert_eq!(files.len(), 7);
94        assert!(paths.contains(&"Cargo.toml"));
95        assert!(paths.contains(&"src/lib.rs"));
96        assert!(paths.contains(&"Camel.plugin.toml"));
97        assert!(paths.contains(&"README.md"));
98        assert!(paths.contains(&".gitignore"));
99        assert!(paths.contains(&"wit/camel-bean.wit"));
100        assert!(paths.contains(&"wit/camel-plugin.wit"));
101    }
102
103    #[test]
104    fn cargo_template_contains_workspace_and_wit_bindgen() {
105        let cargo = cargo_toml("acme-bean");
106
107        assert!(cargo.contains("name = \"acme-bean\""));
108        assert!(cargo.contains("crate-type = [\"cdylib\"]"));
109        assert!(cargo.contains("[workspace]"));
110        assert!(cargo.contains("wit-bindgen = \"0.57\""));
111        assert!(!cargo.contains("camel-wasm-sdk"));
112    }
113
114    #[test]
115    fn lib_template_uses_wit_bindgen_and_bean_world() {
116        let lib = lib_rs("acme-bean");
117
118        assert!(lib.contains("wit_bindgen::generate!"));
119        assert!(lib.contains("world: \"bean\""));
120        assert!(lib.contains("path: \"../wit\""));
121        assert!(lib.contains("impl Guest for AcmeBean"));
122        assert!(lib.contains("fn methods() -> Vec<String>"));
123        assert!(lib.contains("fn invoke("));
124        assert!(lib.contains("Hello from acme-bean: {text}"));
125        assert!(lib.contains("bindings::export!(AcmeBean with_types_in bindings);"));
126        assert!(!lib.contains("camel_wasm_sdk"));
127    }
128
129    #[test]
130    fn plugin_toml_contains_expected_keys() {
131        let plugin = plugin_toml("acme-bean", "bean");
132
133        assert!(plugin.contains("type = \"bean\""));
134        assert!(plugin.contains("entry = \"acme-bean.wasm\""));
135    }
136
137    #[test]
138    fn wit_templates_include_expected_worlds() {
139        let bean_wit = camel_bean_wit();
140        let plugin_wit = camel_plugin_wit();
141
142        assert!(bean_wit.contains("world bean"));
143        assert!(bean_wit.contains("import host;"));
144        assert!(plugin_wit.contains("interface types"));
145        assert!(plugin_wit.contains("interface host"));
146    }
147
148    #[test]
149    fn gitignore_contains_target_and_plugins() {
150        let files = bean_files("acme-bean");
151        let gitignore = files
152            .iter()
153            .find(|f| f.path == ".gitignore")
154            .expect("gitignore");
155        assert!(gitignore.content.contains("target"));
156        assert!(gitignore.content.contains("plugins/"));
157    }
158}