use rhai::{Engine, EvalAltResult};
pub fn format(engine: &Engine, err: &EvalAltResult) -> String {
let default = err.to_string();
let EvalAltResult::ErrorFunctionNotFound(sig, _pos) = err else {
return default;
};
let Some((name, called_with)) = sig.split_once(' ') else {
return default;
};
let mut overloads = collect_overloads(engine, name);
if overloads.is_empty() {
return default;
}
overloads.sort();
overloads.dedup();
let mut out = format!(
"{default}\n\
note: `{name}` is defined, but no overload accepts {called_with}.\n\
hint: check that you're passing the expected argument types — \
e.g. an http() response is a map, so pass `r.body` (string) \
to functions that take a string."
);
out.push_str("\nAvailable overloads:");
for sig in &overloads {
out.push_str("\n ");
out.push_str(sig);
}
out
}
fn collect_overloads(engine: &Engine, name: &str) -> Vec<String> {
let mut out = Vec::new();
for sig in engine.gen_fn_signatures(false) {
let Some(sig_name) = sig.split('(').next() else {
continue;
};
if sig_name.trim() == name {
out.push(simplify_signature(&sig));
}
}
out
}
pub(crate) fn simplify_signature(sig: &str) -> String {
let bytes = sig.as_bytes();
let mut depth = 0i32;
let mut close_args = None;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close_args = Some(i);
break;
}
}
_ => {}
}
}
match close_args {
Some(i) => sig[..=i].to_string(),
None => sig.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::Engine;
#[test]
fn simplify_strips_rust_return_type() {
assert_eq!(
simplify_signature(
"json_parse(_: string) -> core::result::Result<rhai::types::dynamic::Dynamic,alloc::boxed::Box<rhai::types::error::EvalAltResult>>"
),
"json_parse(_: string)"
);
assert_eq!(
simplify_signature("trim(_: string) -> string"),
"trim(_: string)"
);
}
#[test]
fn hint_lists_overloads_for_wrong_arg_types() {
let mut engine = Engine::new();
engine.register_fn("greet", |s: &str| -> String { format!("hi {s}") });
let err = engine.eval::<String>("greet(42)").unwrap_err();
let msg = format(&engine, &err);
assert!(msg.contains("`greet` is defined"));
assert!(msg.contains("(i64)"));
assert!(msg.contains("greet(_: string)"));
}
#[test]
fn truly_unknown_name_keeps_default_message() {
let engine = Engine::new();
let err = engine.eval::<()>("no_such_thing(1)").unwrap_err();
let msg = format(&engine, &err);
assert!(!msg.contains("note:"));
assert!(msg.starts_with("Function not found"));
}
}