1use camel_api::Value;
2use camel_api::exchange::Exchange;
3use camel_language_api::{Expression, Language, LanguageError, MutatingExpression, Predicate};
4use rhai::{Engine, Scope};
5use std::sync::{Arc, RwLock};
6
7const DEFAULT_MAX_OPERATIONS: u64 = 100_000;
9const DEFAULT_MAX_STRING_SIZE: usize = 1_048_576; const DEFAULT_MAX_ARRAY_SIZE: usize = 10_000;
13const DEFAULT_MAX_MAP_SIZE: usize = 10_000;
15
16pub struct RhaiLanguage {
39 engine: Engine,
40}
41
42impl RhaiLanguage {
43 pub fn new() -> Self {
44 let engine = Self::create_base_engine();
45 Self { engine }
46 }
47
48 fn create_base_engine() -> Engine {
50 let mut engine = Engine::new();
51 engine.set_max_expr_depths(64, 32);
52 engine.set_max_operations(DEFAULT_MAX_OPERATIONS);
53 engine.set_max_string_size(DEFAULT_MAX_STRING_SIZE);
54 engine.set_max_array_size(DEFAULT_MAX_ARRAY_SIZE);
55 engine.set_max_map_size(DEFAULT_MAX_MAP_SIZE);
56 engine
57 }
58
59 fn make_scope(exchange: &Exchange) -> (Scope<'static>, rhai::Map, rhai::Map) {
61 let mut scope = Scope::new();
62 let body = exchange.input.body.as_text().unwrap_or("").to_string();
63 scope.push("body", body);
64
65 let mut headers = rhai::Map::new();
66 for (k, v) in &exchange.input.headers {
67 headers.insert(k.clone().into(), json_to_dynamic(v));
68 }
69 scope.push("headers", headers.clone());
70
71 let mut properties = rhai::Map::new();
72 for (k, v) in &exchange.properties {
73 properties.insert(k.clone().into(), json_to_dynamic(v));
74 }
75
76 (scope, headers, properties)
77 }
78
79 fn create_eval_engine(headers: rhai::Map, properties: rhai::Map) -> Engine {
83 let mut engine = Self::create_base_engine();
84
85 let h = Arc::new(RwLock::new(headers));
87
88 let h_read = h.clone();
89 engine.register_fn("header", move |name: String| -> rhai::Dynamic {
90 h_read
91 .read()
92 .unwrap_or_else(|e| e.into_inner())
93 .get(name.as_str())
94 .cloned()
95 .unwrap_or(rhai::Dynamic::UNIT)
96 });
97
98 let h_write = h.clone();
99 engine.register_fn("set_header", move |name: String, value: rhai::Dynamic| {
100 h_write
101 .write()
102 .unwrap_or_else(|e| e.into_inner())
103 .insert(name.into(), value);
104 });
105
106 let p = Arc::new(RwLock::new(properties));
108
109 let p_read = p.clone();
110 engine.register_fn("property", move |name: String| -> rhai::Dynamic {
111 p_read
112 .read()
113 .unwrap_or_else(|e| e.into_inner())
114 .get(name.as_str())
115 .cloned()
116 .unwrap_or(rhai::Dynamic::UNIT)
117 });
118
119 let p_write = p.clone();
120 engine.register_fn("set_property", move |name: String, value: rhai::Dynamic| {
121 p_write
122 .write()
123 .unwrap_or_else(|e| e.into_inner())
124 .insert(name.into(), value);
125 });
126
127 engine
128 }
129
130 fn eval_to_value(script: &str, exchange: &Exchange) -> Result<Value, LanguageError> {
131 let (mut scope, headers, properties) = Self::make_scope(exchange);
132 let engine = Self::create_eval_engine(headers, properties);
133
134 let result: rhai::Dynamic = engine
135 .eval_with_scope::<rhai::Dynamic>(&mut scope, script)
136 .map_err(|e| LanguageError::EvalError(e.to_string()))?;
137
138 dynamic_to_json(result)
139 }
140}
141
142fn json_to_dynamic(v: &Value) -> rhai::Dynamic {
144 match v {
145 Value::String(s) => rhai::Dynamic::from(s.clone()),
146 Value::Number(n) => {
147 if let Some(i) = n.as_i64() {
148 rhai::Dynamic::from(i)
149 } else if let Some(f) = n.as_f64() {
150 rhai::Dynamic::from_float(f)
151 } else {
152 rhai::Dynamic::from(n.to_string())
153 }
154 }
155 Value::Bool(b) => rhai::Dynamic::from(*b),
156 Value::Null => rhai::Dynamic::UNIT,
157 Value::Array(arr) => {
158 let rhai_arr: rhai::Array = arr.iter().map(json_to_dynamic).collect();
159 rhai::Dynamic::from(rhai_arr)
160 }
161 Value::Object(obj) => {
162 let mut rhai_map = rhai::Map::new();
163 for (k, v) in obj {
164 rhai_map.insert(k.clone().into(), json_to_dynamic(v));
165 }
166 rhai::Dynamic::from(rhai_map)
167 }
168 }
169}
170
171fn dynamic_to_json(d: rhai::Dynamic) -> Result<Value, LanguageError> {
172 if d.is_string() {
173 Ok(Value::String(d.cast::<String>()))
174 } else if d.is::<bool>() {
175 Ok(Value::Bool(d.cast::<bool>()))
176 } else if d.is_float() {
177 Ok(Value::from(d.cast::<f64>()))
178 } else if d.is_int() {
179 Ok(Value::from(d.cast::<i64>()))
180 } else if d.is_unit() {
181 Ok(Value::Null)
182 } else {
183 Ok(Value::String(d.to_string()))
184 }
185}
186
187fn dynamic_to_value(d: rhai::Dynamic) -> Value {
189 if d.is_string() {
190 Value::String(d.cast::<String>())
191 } else if d.is::<bool>() {
192 Value::Bool(d.cast::<bool>())
193 } else if d.is_int() {
194 Value::from(d.cast::<i64>())
195 } else if d.is_float() {
196 Value::from(d.cast::<f64>())
197 } else if d.is_unit() {
198 Value::Null
199 } else {
200 Value::String(d.to_string())
201 }
202}
203
204fn rhai_map_to_value_map(map: &rhai::Map) -> std::collections::HashMap<String, Value> {
206 let mut result = std::collections::HashMap::new();
207 for (k, v) in map {
208 result.insert(k.to_string(), dynamic_to_value(v.clone()));
209 }
210 result
211}
212
213struct RhaiExpression {
214 script: String,
215}
216
217struct RhaiPredicate {
218 script: String,
219}
220
221impl Expression for RhaiExpression {
222 fn evaluate(&self, exchange: &Exchange) -> Result<Value, LanguageError> {
223 RhaiLanguage::eval_to_value(&self.script, exchange)
224 }
225}
226
227impl Predicate for RhaiPredicate {
228 fn matches(&self, exchange: &Exchange) -> Result<bool, LanguageError> {
229 let val = RhaiLanguage::eval_to_value(&self.script, exchange)?;
230 Ok(match &val {
231 Value::Bool(b) => *b,
232 Value::Null => false,
233 _ => true,
234 })
235 }
236}
237
238struct RhaiMutatingExpression {
261 script: String,
262}
263
264impl MutatingExpression for RhaiMutatingExpression {
265 fn evaluate(&self, exchange: &mut Exchange) -> Result<Value, LanguageError> {
266 let original_headers = exchange.input.headers.clone();
268 let original_properties = exchange.properties.clone();
269 let original_body = exchange.input.body.clone();
270
271 let mut scope = Scope::new();
273
274 let mut headers_map = rhai::Map::new();
275 for (k, v) in &exchange.input.headers {
276 headers_map.insert(k.clone().into(), json_to_dynamic(v));
277 }
278
279 let mut properties_map = rhai::Map::new();
280 for (k, v) in &exchange.properties {
281 properties_map.insert(k.clone().into(), json_to_dynamic(v));
282 }
283
284 let body_str = exchange.input.body.as_text().unwrap_or("").to_string();
285
286 scope.push("headers", headers_map);
287 scope.push("properties", properties_map);
288 scope.push("body", body_str);
289
290 let engine = RhaiLanguage::create_base_engine();
292
293 let result: rhai::Dynamic = match engine.eval_with_scope(&mut scope, &self.script) {
294 Ok(v) => v,
295 Err(e) => {
296 exchange.input.headers = original_headers;
297 exchange.properties = original_properties;
298 exchange.input.body = original_body;
299 return Err(LanguageError::EvalError(e.to_string()));
300 }
301 };
302
303 if let Some(h) = scope.get_value::<rhai::Map>("headers") {
305 exchange.input.headers = rhai_map_to_value_map(&h);
306 }
307 if let Some(p) = scope.get_value::<rhai::Map>("properties") {
308 exchange.properties = rhai_map_to_value_map(&p);
309 }
310 if let Some(b) = scope.get_value::<String>("body") {
311 exchange.input.body = camel_api::Body::Text(b);
312 }
313
314 Ok(dynamic_to_value(result))
315 }
316}
317
318impl Language for RhaiLanguage {
319 fn name(&self) -> &'static str {
320 "rhai"
321 }
322
323 fn create_expression(&self, script: &str) -> Result<Box<dyn Expression>, LanguageError> {
324 self.engine
326 .compile(script)
327 .map_err(|e| LanguageError::ParseError {
328 expr: script.to_string(),
329 reason: e.to_string(),
330 })?;
331 Ok(Box::new(RhaiExpression {
332 script: script.to_string(),
333 }))
334 }
335
336 fn create_predicate(&self, script: &str) -> Result<Box<dyn Predicate>, LanguageError> {
337 self.engine
338 .compile(script)
339 .map_err(|e| LanguageError::ParseError {
340 expr: script.to_string(),
341 reason: e.to_string(),
342 })?;
343 Ok(Box::new(RhaiPredicate {
344 script: script.to_string(),
345 }))
346 }
347
348 fn create_mutating_expression(
353 &self,
354 script: &str,
355 ) -> Result<Box<dyn MutatingExpression>, LanguageError> {
356 self.engine
357 .compile(script)
358 .map_err(|e| LanguageError::ParseError {
359 expr: script.to_string(),
360 reason: e.to_string(),
361 })?;
362 Ok(Box::new(RhaiMutatingExpression {
363 script: script.to_string(),
364 }))
365 }
366}
367
368impl Default for RhaiLanguage {
369 fn default() -> Self {
370 Self::new()
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use camel_api::{Value, exchange::Exchange, message::Message};
377 use camel_language_api::Language;
378
379 use super::RhaiLanguage;
380
381 fn exchange_with_header(key: &str, val: &str) -> Exchange {
382 let mut msg = Message::default();
383 msg.set_header(key, Value::String(val.to_string()));
384 Exchange::new(msg)
385 }
386
387 fn exchange_with_body(body: &str) -> Exchange {
388 Exchange::new(Message::new(body))
389 }
390
391 #[test]
392 fn test_rhai_predicate_simple() {
393 let lang = RhaiLanguage::new();
394 let pred = lang
395 .create_predicate(r#"header("type") == "order""#)
396 .unwrap();
397 let ex = exchange_with_header("type", "order");
398 assert!(pred.matches(&ex).unwrap());
399 }
400
401 #[test]
402 fn test_rhai_predicate_false() {
403 let lang = RhaiLanguage::new();
404 let pred = lang
405 .create_predicate(r#"header("type") == "order""#)
406 .unwrap();
407 let ex = exchange_with_header("type", "invoice");
408 assert!(!pred.matches(&ex).unwrap());
409 }
410
411 #[test]
412 fn test_rhai_expression_body() {
413 let lang = RhaiLanguage::new();
414 let expr = lang.create_expression("body").unwrap();
415 let ex = exchange_with_body("hello");
416 let val = expr.evaluate(&ex).unwrap();
417 assert_eq!(val, Value::String("hello".to_string()));
418 }
419
420 #[test]
421 fn test_rhai_expression_concat() {
422 let lang = RhaiLanguage::new();
423 let expr = lang.create_expression(r#"body + " world""#).unwrap();
424 let ex = exchange_with_body("hello");
425 let val = expr.evaluate(&ex).unwrap();
426 assert_eq!(val, Value::String("hello world".to_string()));
427 }
428
429 #[test]
430 fn test_rhai_set_header_visible_within_script() {
431 let lang = RhaiLanguage::new();
432 let expr = lang
433 .create_expression(
434 r#"
435 set_header("done", "yes");
436 header("done")
437 "#,
438 )
439 .unwrap();
440 let ex = exchange_with_body("test");
441 let val = expr.evaluate(&ex).unwrap();
442 assert_eq!(val, Value::String("yes".to_string()));
443 }
444
445 #[test]
446 fn test_rhai_property_access() {
447 let lang = RhaiLanguage::new();
448 let expr = lang.create_expression(r#"property("myProp")"#).unwrap();
449 let mut ex = exchange_with_body("test");
450 ex.set_property("myProp".to_string(), Value::String("propVal".to_string()));
451 let val = expr.evaluate(&ex).unwrap();
452 assert_eq!(val, Value::String("propVal".to_string()));
453 }
454
455 #[test]
456 fn test_rhai_set_property_visible_within_script() {
457 let lang = RhaiLanguage::new();
458 let expr = lang
459 .create_expression(
460 r#"
461 set_property("key", "value");
462 property("key")
463 "#,
464 )
465 .unwrap();
466 let ex = exchange_with_body("test");
467 let val = expr.evaluate(&ex).unwrap();
468 assert_eq!(val, Value::String("value".to_string()));
469 }
470
471 #[test]
472 fn test_rhai_missing_header_returns_null() {
473 let lang = RhaiLanguage::new();
474 let expr = lang.create_expression(r#"header("nonexistent")"#).unwrap();
475 let ex = exchange_with_body("test");
476 let val = expr.evaluate(&ex).unwrap();
477 assert_eq!(val, Value::Null);
478 }
479
480 #[test]
481 fn test_rhai_syntax_error() {
482 let lang = RhaiLanguage::new();
483 let result = lang.create_expression("let x = ;");
484 assert!(result.is_err());
485 }
486
487 #[test]
488 fn test_rhai_runtime_error() {
489 let lang = RhaiLanguage::new();
490 let expr = lang.create_expression(r#"nonexistent_fn()"#).unwrap();
492 let ex = exchange_with_body("test");
493 let result = expr.evaluate(&ex);
494 assert!(result.is_err());
495 }
496
497 #[test]
498 fn test_rhai_operations_limit_prevents_infinite_loop() {
499 let lang = RhaiLanguage::new();
500 let expr = lang
502 .create_expression("let x = 0; loop { x += 1; } x")
503 .unwrap();
504 let ex = exchange_with_body("test");
505 let result = expr.evaluate(&ex);
506 assert!(result.is_err(), "should error due to operations limit");
507 let err_msg = format!("{}", result.unwrap_err());
508 assert!(
509 err_msg.contains("operations") || err_msg.contains("limit"),
510 "error should mention operations limit, got: {err_msg}"
511 );
512 }
513
514 #[test]
515 fn test_rhai_empty_body() {
516 let lang = RhaiLanguage::new();
517 let expr = lang.create_expression("body").unwrap();
518 let ex = Exchange::new(Message::default());
519 let val = expr.evaluate(&ex).unwrap();
520 assert_eq!(val, Value::String("".to_string()));
521 }
522
523 #[test]
524 fn test_rhai_numeric_header() {
525 let lang = RhaiLanguage::new();
526 let expr = lang.create_expression(r#"header("count") + 1"#).unwrap();
527 let mut msg = Message::default();
528 msg.set_header("count", Value::Number(41.into()));
529 let ex = Exchange::new(msg);
530 let val = expr.evaluate(&ex).unwrap();
531 assert_eq!(val, Value::from(42));
532 }
533
534 #[test]
535 fn test_rhai_json_array_header_is_native_array() {
536 let lang = RhaiLanguage::new();
539 let expr = lang.create_expression(r#"header("items").len()"#).unwrap();
540 let mut msg = Message::default();
541 msg.set_header(
542 "items",
543 Value::Array(vec![
544 Value::String("a".into()),
545 Value::String("b".into()),
546 Value::String("c".into()),
547 ]),
548 );
549 let ex = Exchange::new(msg);
550 let val = expr.evaluate(&ex).unwrap();
551 assert_eq!(
552 val,
553 Value::from(3_i64),
554 "array len should be 3, not a stringified value"
555 );
556 }
557
558 #[test]
559 fn test_rhai_json_object_header_is_native_map() {
560 let lang = RhaiLanguage::new();
562 let expr = lang.create_expression(r#"header("obj")["key"]"#).unwrap();
563 let mut msg = Message::default();
564 let obj: Value = r#"{"key": "value"}"#.parse().unwrap();
566 msg.set_header("obj", obj);
567 let ex = Exchange::new(msg);
568 let val = expr.evaluate(&ex).unwrap();
569 assert_eq!(val, Value::String("value".to_string()));
570 }
571
572 #[test]
573 fn test_mutating_set_header_propagates_to_exchange() {
574 let lang = RhaiLanguage::new();
575 let expr = lang
576 .create_mutating_expression(r#"headers["tenant"] = "acme""#)
577 .unwrap();
578 let mut ex = Exchange::new(Message::default());
579 expr.evaluate(&mut ex).unwrap();
580 assert_eq!(
581 ex.input.headers.get("tenant"),
582 Some(&Value::String("acme".into()))
583 );
584 }
585
586 #[test]
587 fn test_mutating_set_body_propagates_to_exchange() {
588 let lang = RhaiLanguage::new();
589 let expr = lang
590 .create_mutating_expression(r#"body = "modified""#)
591 .unwrap();
592 let mut ex = Exchange::new(Message::new("original"));
593 expr.evaluate(&mut ex).unwrap();
594 assert_eq!(ex.input.body.as_text(), Some("modified"));
595 }
596
597 #[test]
598 fn test_mutating_set_property_propagates_to_exchange() {
599 let lang = RhaiLanguage::new();
600 let expr = lang
601 .create_mutating_expression(r#"properties["auth"] = "ok""#)
602 .unwrap();
603 let mut ex = Exchange::new(Message::default());
604 expr.evaluate(&mut ex).unwrap();
605 assert_eq!(ex.properties.get("auth"), Some(&Value::String("ok".into())));
606 }
607
608 #[test]
609 fn test_mutating_rollback_on_error() {
610 let lang = RhaiLanguage::new();
611 let expr = lang
612 .create_mutating_expression(r#"headers["x"] = "modified"; throw "error""#)
613 .unwrap();
614 let mut ex = Exchange::new(Message::default());
615 ex.input
616 .headers
617 .insert("x".to_string(), Value::String("original".into()));
618 let result = expr.evaluate(&mut ex);
619 assert!(result.is_err());
620 assert_eq!(
621 ex.input.headers.get("x"),
622 Some(&Value::String("original".into()))
623 );
624 }
625
626 #[test]
627 fn test_mutating_rollback_on_error_includes_body() {
628 let lang = RhaiLanguage::new();
629 let expr = lang
630 .create_mutating_expression(r#"body = "modified"; throw "error""#)
631 .unwrap();
632 let mut ex = Exchange::new(Message::new("original"));
633 let result = expr.evaluate(&mut ex);
634 assert!(result.is_err());
635 assert_eq!(ex.input.body.as_text(), Some("original"));
636 }
637
638 #[test]
639 fn test_mutating_rollback_on_error_includes_property() {
640 let lang = RhaiLanguage::new();
641 let expr = lang
642 .create_mutating_expression(r#"properties["p"] = "modified"; throw "error""#)
643 .unwrap();
644 let mut ex = Exchange::new(Message::default());
645 ex.properties
646 .insert("p".to_string(), Value::String("original".into()));
647 let result = expr.evaluate(&mut ex);
648 assert!(result.is_err());
649 assert_eq!(
650 ex.properties.get("p"),
651 Some(&Value::String("original".into()))
652 );
653 }
654
655 #[test]
656 fn test_mutating_combined_read_write() {
657 let lang = RhaiLanguage::new();
658 let expr = lang
659 .create_mutating_expression(r#"headers["out"] = headers["in"] + "_processed""#)
660 .unwrap();
661 let mut ex = Exchange::new(Message::default());
662 ex.input
663 .headers
664 .insert("in".to_string(), Value::String("value".into()));
665 expr.evaluate(&mut ex).unwrap();
666 assert_eq!(
667 ex.input.headers.get("out"),
668 Some(&Value::String("value_processed".into()))
669 );
670 }
671}