camel_language_js/engines/
boa.rs1use boa_engine::{Context, JsValue, Source, js_string};
8
9use crate::{
10 bindings,
11 engine::{JsEngine, JsEvalResult, JsExchange},
12 error::JsLanguageError,
13 value::js_to_value,
14};
15
16#[derive(Debug, Clone, Default)]
21pub struct BoaEngine;
22
23impl BoaEngine {
24 #[must_use]
25 pub const fn new() -> Self {
26 Self
27 }
28}
29
30impl JsEngine for BoaEngine {
31 fn eval(&self, source: &str, exchange: JsExchange) -> Result<JsEvalResult, JsLanguageError> {
32 let mut ctx = Context::default();
33
34 bindings::register_console(&mut ctx);
36
37 let camel_obj = bindings::build_camel_global(&exchange, &mut ctx).map_err(|e| {
39 JsLanguageError::Execution {
40 message: e.to_string(),
41 }
42 })?;
43
44 ctx.global_object()
45 .set(
46 js_string!("camel"),
47 JsValue::from(camel_obj),
48 false,
49 &mut ctx,
50 )
51 .map_err(|e| JsLanguageError::Execution {
52 message: format!("camel global set: {e}"),
53 })?;
54
55 let result = ctx
57 .eval(Source::from_bytes(source.as_bytes()))
58 .map_err(|e| JsLanguageError::Execution {
59 message: e.to_string(),
60 })?;
61
62 let return_value = js_to_value(&result, &mut ctx)?;
63
64 let modified = bindings::extract_camel_state(&mut ctx)?;
66
67 Ok(JsEvalResult {
68 return_value,
69 headers: modified.headers,
70 body: modified.body,
71 properties: modified.properties,
72 })
73 }
74
75 fn validate(&self, source: &str) -> Result<(), JsLanguageError> {
76 let mut ctx = Context::default();
79 let _script =
80 boa_engine::Script::parse(Source::from_bytes(source.as_bytes()), None, &mut ctx)
81 .map_err(|e| JsLanguageError::Parse {
82 message: e.to_string(),
83 })?;
84
85 Ok(())
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use serde_json::json;
93
94 fn make_exchange() -> JsExchange {
95 JsExchange::from_headers_body_properties(
96 [("foo".to_string(), json!("bar"))].into_iter().collect(),
97 json!("hello"),
98 [("key".to_string(), json!("val"))].into_iter().collect(),
99 )
100 }
101
102 #[test]
103 fn test_eval_return_value() {
104 let engine = BoaEngine::new();
105 let result = engine.eval("1 + 1", JsExchange::default()).unwrap();
106 assert_eq!(result.return_value.as_i64().unwrap(), 2);
107 }
108
109 #[test]
110 fn test_eval_header_access() {
111 let engine = BoaEngine::new();
112 let ex = make_exchange();
113 let result = engine.eval("camel.headers.get('foo')", ex).unwrap();
114 assert_eq!(result.return_value.as_str().unwrap(), "bar");
115 }
116
117 #[test]
118 fn test_eval_body_getter() {
119 let engine = BoaEngine::new();
120 let ex = make_exchange();
121 let result = engine.eval("camel.body", ex).unwrap();
122 assert_eq!(result.return_value.as_str().unwrap(), "hello");
123 }
124
125 #[test]
126 fn test_mutating_set_header_propagates() {
127 let engine = BoaEngine::new();
128 let ex = make_exchange();
129 let result = engine
130 .eval("camel.headers.set('newkey', 'newval'); 'done'", ex)
131 .unwrap();
132 assert_eq!(result.return_value.as_str().unwrap(), "done");
133 assert_eq!(
134 result.headers.get("newkey").unwrap().as_str().unwrap(),
135 "newval"
136 );
137 }
138
139 #[test]
140 fn test_mutating_body_propagates() {
141 let engine = BoaEngine::new();
142 let ex = make_exchange();
143 let result = engine
144 .eval("camel.body = 'modified'; camel.body", ex)
145 .unwrap();
146 assert_eq!(result.body.as_str().unwrap(), "modified");
147 }
148
149 #[test]
150 fn test_console_log_no_crash() {
151 let engine = BoaEngine::new();
152 let result = engine
153 .eval("console.log('test'); 42", JsExchange::default())
154 .unwrap();
155 assert_eq!(result.return_value.as_i64().unwrap(), 42);
156 }
157
158 #[test]
159 fn test_validate_valid() {
160 let engine = BoaEngine::new();
161 assert!(engine.validate("let x = 1 + 1;").is_ok());
162 }
163
164 #[test]
165 fn test_validate_invalid() {
166 let engine = BoaEngine::new();
167 assert!(engine.validate("let x = {{{").is_err());
168 }
169
170 #[test]
171 fn test_eval_property_access() {
172 let engine = BoaEngine::new();
173 let ex = make_exchange();
174 let result = engine.eval("camel.properties.get('key')", ex).unwrap();
175 assert_eq!(result.return_value.as_str().unwrap(), "val");
176 }
177
178 #[test]
179 fn test_eval_runtime_error_returns_err() {
180 let engine = BoaEngine::new();
181 let result = engine.eval("throw new Error('boom')", JsExchange::default());
182 assert!(result.is_err());
183 let msg = result.unwrap_err().to_string();
184 assert!(
185 msg.contains("boom")
186 || msg.to_lowercase().contains("execution")
187 || msg.to_lowercase().contains("error")
188 );
189 }
190
191 #[test]
192 fn test_eval_syntax_error_returns_err() {
193 let engine = BoaEngine::new();
194 let result = engine.eval("let x = {{{", JsExchange::default());
195 assert!(result.is_err());
196 }
197
198 #[test]
199 fn test_eval_missing_header_returns_undefined() {
200 let engine = BoaEngine::new();
201 let ex = make_exchange();
202 let result = engine.eval("camel.headers.get('nonexistent')", ex).unwrap();
204 assert!(result.return_value.is_null());
205 }
206
207 #[test]
208 fn test_properties_mutation_propagates() {
209 let engine = BoaEngine::new();
210 let ex = make_exchange();
211 let result = engine
212 .eval("camel.properties.set('newprop', 'newval'); 'done'", ex)
213 .unwrap();
214 assert_eq!(result.return_value.as_str().unwrap(), "done");
215 assert_eq!(
216 result.properties.get("newprop").unwrap().as_str().unwrap(),
217 "newval"
218 );
219 }
220
221 #[test]
222 fn test_headers_keys() {
223 let engine = BoaEngine::new();
224 let ex = make_exchange();
225 let result = engine.eval("camel.headers.keys()", ex).unwrap();
226 let keys: Vec<&str> = result
227 .return_value
228 .as_array()
229 .unwrap()
230 .iter()
231 .map(|v| v.as_str().unwrap())
232 .collect();
233 assert!(keys.contains(&"foo"));
234 }
235
236 #[test]
237 fn test_headers_has() {
238 let engine = BoaEngine::new();
239 let ex = make_exchange();
240 let r1 = engine.eval("camel.headers.has('foo')", ex.clone()).unwrap();
241 assert!(r1.return_value.as_bool().unwrap());
242 let r2 = engine.eval("camel.headers.has('missing')", ex).unwrap();
243 assert!(!r2.return_value.as_bool().unwrap());
244 }
245
246 #[test]
247 fn test_headers_remove() {
248 let engine = BoaEngine::new();
249 let ex = make_exchange();
250 let result = engine
251 .eval("camel.headers.remove('foo'); camel.headers.has('foo')", ex)
252 .unwrap();
253 assert!(!result.return_value.as_bool().unwrap());
254 assert!(!result.headers.contains_key("foo"));
255 }
256}