use rhai::{Array, Dynamic, Engine, Map};
use std::cell::{Cell, RefCell};
thread_local! {
static PENDING_EMISSIONS: RefCell<Vec<Map>> = const { RefCell::new(Vec::new()) };
static SUPPRESS_CURRENT: Cell<bool> = const { Cell::new(false) };
static EMIT_STRICT: Cell<bool> = const { Cell::new(false) };
}
pub fn get_and_clear_pending_emissions() -> Vec<Map> {
PENDING_EMISSIONS.with(|emissions| {
let mut vec = emissions.borrow_mut();
let result = vec.clone();
vec.clear();
result
})
}
pub fn should_suppress_current_event() -> bool {
SUPPRESS_CURRENT.with(|suppress| suppress.get())
}
pub fn clear_suppression_flag() {
SUPPRESS_CURRENT.with(|suppress| suppress.set(false));
}
pub fn set_emit_strict(strict: bool) {
EMIT_STRICT.with(|flag| flag.set(strict));
}
fn is_emit_strict() -> bool {
EMIT_STRICT.with(|flag| flag.get())
}
pub fn emit_each_single(items: Dynamic) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
emit_each_impl(items, Dynamic::UNIT)
}
pub fn emit_each_with_base(
items: Dynamic,
base: Dynamic,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
emit_each_impl(items, base)
}
fn emit_each_impl(
items_val: Dynamic,
base_val: Dynamic,
) -> Result<Dynamic, Box<rhai::EvalAltResult>> {
let strict = is_emit_strict();
let items = match items_val.clone().try_cast::<Array>() {
Some(array) => array,
None => {
if strict {
return Err(format!(
"emit_each(): items must be array<map>, got {}",
items_val.type_name()
)
.into());
} else {
eprintln!(
"🔸 emit_each(): items must be array<map>; got {}; returning 0",
items_val.type_name()
);
return Ok(Dynamic::from(0i64));
}
}
};
let base = if base_val.is_unit() {
None
} else {
match base_val.clone().try_cast::<Map>() {
Some(map) => Some(map),
None => {
if strict {
return Err(format!(
"emit_each(): base must be map, got {}",
base_val.type_name()
)
.into());
} else {
eprintln!(
"🔸 emit_each(): base must be map; got {}; treating as empty",
base_val.type_name()
);
None
}
}
}
};
let mut emitted = 0i64;
SUPPRESS_CURRENT.with(|suppress| suppress.set(true));
for (i, item) in items.iter().enumerate() {
let item_map = match item.clone().try_cast::<Map>() {
Some(map) => map,
None => {
if strict {
return Err(format!(
"emit_each(): items[{}] is not a map (got {})",
i,
item.type_name()
)
.into());
} else {
eprintln!(
"🔸 emit_each(): skipping items[{}], expected map (got {})",
i,
item.type_name()
);
continue;
}
}
};
let event_map = match &base {
Some(base_map) => {
let mut result = base_map.clone();
for (key, value) in item_map {
result.insert(key, value);
}
result
}
None => item_map,
};
PENDING_EMISSIONS.with(|emissions| {
emissions.borrow_mut().push(event_map);
});
emitted += 1;
}
Ok(Dynamic::from(emitted))
}
pub fn register_functions(engine: &mut Engine) {
engine.register_fn("emit_each", emit_each_single);
engine.register_fn("emit_each", emit_each_with_base);
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::{Array, Engine, Map};
fn setup_test() {
PENDING_EMISSIONS.with(|emissions| emissions.borrow_mut().clear());
SUPPRESS_CURRENT.with(|suppress| suppress.set(false));
}
#[test]
fn test_emit_each_empty_array() {
setup_test();
let empty_array = Dynamic::from(Array::new());
let result = emit_each_single(empty_array).unwrap();
assert_eq!(result.as_int().unwrap(), 0);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 0);
}
#[test]
fn test_emit_each_basic_array() {
setup_test();
let mut map1 = Map::new();
map1.insert("name".into(), Dynamic::from("alice"));
map1.insert("age".into(), Dynamic::from(25i64));
let mut map2 = Map::new();
map2.insert("name".into(), Dynamic::from("bob"));
map2.insert("age".into(), Dynamic::from(30i64));
let array = vec![Dynamic::from(map1), Dynamic::from(map2)];
let items = Dynamic::from(array);
let result = emit_each_single(items).unwrap();
assert_eq!(result.as_int().unwrap(), 2);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 2);
let first = &emissions[0];
assert_eq!(
first.get("name").unwrap().clone().into_string().unwrap(),
"alice"
);
assert_eq!(first.get("age").unwrap().as_int().unwrap(), 25);
let second = &emissions[1];
assert_eq!(
second.get("name").unwrap().clone().into_string().unwrap(),
"bob"
);
assert_eq!(second.get("age").unwrap().as_int().unwrap(), 30);
}
#[test]
fn test_emit_each_with_base() {
setup_test();
let mut base = Map::new();
base.insert("host".into(), Dynamic::from("server1"));
base.insert("app".into(), Dynamic::from("myapp"));
let mut item1 = Map::new();
item1.insert("id".into(), Dynamic::from(1i64));
item1.insert("app".into(), Dynamic::from("override_app"));
let mut item2 = Map::new();
item2.insert("id".into(), Dynamic::from(2i64));
let array = vec![Dynamic::from(item1), Dynamic::from(item2)];
let items = Dynamic::from(array);
let base_dynamic = Dynamic::from(base);
let result = emit_each_with_base(items, base_dynamic).unwrap();
assert_eq!(result.as_int().unwrap(), 2);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 2);
let first = &emissions[0];
assert_eq!(
first.get("host").unwrap().clone().into_string().unwrap(),
"server1"
);
assert_eq!(
first.get("app").unwrap().clone().into_string().unwrap(),
"override_app"
);
assert_eq!(first.get("id").unwrap().as_int().unwrap(), 1);
let second = &emissions[1];
assert_eq!(
second.get("host").unwrap().clone().into_string().unwrap(),
"server1"
);
assert_eq!(
second.get("app").unwrap().clone().into_string().unwrap(),
"myapp"
);
assert_eq!(second.get("id").unwrap().as_int().unwrap(), 2);
}
#[test]
fn test_emit_each_invalid_items_type() {
setup_test();
let not_array = Dynamic::from("not an array");
let result = emit_each_single(not_array).unwrap();
assert_eq!(result.as_int().unwrap(), 0);
assert!(!should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 0);
}
#[test]
fn test_emit_each_invalid_base_type() {
setup_test();
let mut map1 = Map::new();
map1.insert("name".into(), Dynamic::from("alice"));
let array = vec![Dynamic::from(map1)];
let items = Dynamic::from(array);
let invalid_base = Dynamic::from("not a map");
let result = emit_each_with_base(items, invalid_base).unwrap();
assert_eq!(result.as_int().unwrap(), 1);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 1);
assert_eq!(
emissions[0]
.get("name")
.unwrap()
.clone()
.into_string()
.unwrap(),
"alice"
);
}
#[test]
fn test_emit_each_mixed_array_resilient() {
setup_test();
let mut valid_map = Map::new();
valid_map.insert("valid".into(), Dynamic::from(true));
let array = vec![
Dynamic::from(valid_map),
Dynamic::from("not a map"),
Dynamic::from(42i64),
];
let items = Dynamic::from(array);
let result = emit_each_single(items).unwrap();
assert_eq!(result.as_int().unwrap(), 1);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 1);
assert!(emissions[0].get("valid").unwrap().as_bool().unwrap());
}
#[test]
fn test_emit_each_empty_maps() {
setup_test();
let empty_map = Map::new();
let array = vec![Dynamic::from(empty_map)];
let items = Dynamic::from(array);
let result = emit_each_single(items).unwrap();
assert_eq!(result.as_int().unwrap(), 1);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 1);
assert!(emissions[0].is_empty()); }
#[test]
fn test_emit_each_rhai_integration() {
setup_test();
let mut engine = Engine::new();
register_functions(&mut engine);
let result = engine
.eval::<i64>(
r#"
let items = [#{name: "alice"}, #{name: "bob"}];
emit_each(items)
"#,
)
.unwrap();
assert_eq!(result, 2);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 2);
clear_suppression_flag();
let result = engine
.eval::<i64>(
r#"
let items = [#{id: 1}, #{id: 2}];
let base = #{host: "server1"};
emit_each(items, base)
"#,
)
.unwrap();
assert_eq!(result, 2);
assert!(should_suppress_current_event());
let emissions = get_and_clear_pending_emissions();
assert_eq!(emissions.len(), 2);
assert_eq!(
emissions[0]
.get("host")
.unwrap()
.clone()
.into_string()
.unwrap(),
"server1"
);
assert_eq!(
emissions[1]
.get("host")
.unwrap()
.clone()
.into_string()
.unwrap(),
"server1"
);
assert_eq!(emissions[0].get("id").unwrap().as_int().unwrap(), 1);
assert_eq!(emissions[1].get("id").unwrap().as_int().unwrap(), 2);
}
}