use std::cell::RefCell;
use std::rc::Rc;
use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use boa_engine::{js_string, Context, JsError, JsResult, JsString, JsValue, NativeFunction, Source};
use serde_json::Value;
pub use boa_engine;
pub const LOOP_ITERATION_LIMIT: u64 = 10_000_000;
pub const RECURSION_LIMIT: usize = 512;
pub type Logs = Rc<RefCell<Vec<String>>>;
#[derive(Debug, Clone)]
pub struct Success {
pub value: Value,
pub logs: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Failure {
pub message: String,
pub logs: Vec<String>,
}
pub fn call(
source: &str,
file: &str,
function: &str,
input: &Value,
install: &dyn Fn(&mut Context, &Logs) -> JsResult<()>,
) -> Result<Success, Failure> {
let mut context = Context::default();
let limits = context.runtime_limits_mut();
limits.set_loop_iteration_limit(LOOP_ITERATION_LIMIT);
limits.set_recursion_limit(RECURSION_LIMIT);
let logs: Logs = Rc::new(RefCell::new(Vec::new()));
let fail = |message: String, logs: &Logs| Failure {
message,
logs: logs.borrow().clone(),
};
if let Err(error) = install_console(&mut context, &logs).and_then(|()| install(&mut context, &logs)) {
return Err(fail(format!("setting up the script host failed: {}", describe(error, &mut context)), &logs));
}
let path = std::path::Path::new(file);
if let Err(error) = context.eval(Source::from_bytes(source).with_path(path)) {
let message = describe(error, &mut context);
return Err(fail(format!("{file}: {message}"), &logs));
}
let global = context.global_object();
let callee = match global.get(JsString::from(function), &mut context) {
Ok(value) => value,
Err(error) => return Err(fail(describe(error, &mut context), &logs)),
};
let Some(callee) = callee.as_callable() else {
return Err(fail(format!("{file} defines no function {function}()"), &logs));
};
let argument = match JsValue::from_json(input, &mut context) {
Ok(value) => value,
Err(error) => return Err(fail(describe(error, &mut context), &logs)),
};
let returned = match callee.call(&JsValue::undefined(), &[argument], &mut context) {
Ok(value) => value,
Err(error) => return Err(fail(describe(error, &mut context), &logs)),
};
let _ = context.run_jobs();
let value = match returned.to_json(&mut context) {
Ok(json) => json.unwrap_or(Value::Null),
Err(error) => {
return Err(fail(
format!("{function}() returned a value that is not JSON: {}", describe(error, &mut context)),
&logs,
))
}
};
let logs = logs.borrow().clone();
Ok(Success { value, logs })
}
pub fn describe(error: JsError, context: &mut Context) -> String {
if let Some(value) = error.as_opaque() {
if let Some(text) = value.as_string() {
return text.to_std_string_escaped();
}
}
match error.try_native(context) {
Ok(native) if !native.message().is_empty() => native.message().to_string(),
Ok(native) => native.to_string(),
Err(_) => {
if let Some(value) = error.as_opaque() {
if let Ok(Some(json)) = value.to_json(context) {
return json.to_string();
}
}
error.to_string()
}
}
}
pub fn text_of(value: &JsValue, context: &mut Context) -> String {
if let Some(text) = value.as_string() {
return text.to_std_string_escaped();
}
match value.to_json(context) {
Ok(Some(json)) => json.to_string(),
Ok(None) => "undefined".to_string(),
Err(_) => value.display().to_string(),
}
}
fn install_console(context: &mut Context, logs: &Logs) -> JsResult<()> {
let make = |prefix: &'static str| {
let logs = logs.clone();
unsafe {
NativeFunction::from_closure(move |_, args, context| {
let line = args
.iter()
.map(|value| text_of(value, context))
.collect::<Vec<_>>()
.join(" ");
logs.borrow_mut().push(format!("{prefix}{line}"));
Ok(JsValue::undefined())
})
}
};
let console = ObjectInitializer::new(context)
.function(make(""), js_string!("log"), 0)
.function(make(""), js_string!("info"), 0)
.function(make("warn: "), js_string!("warn"), 0)
.function(make("error: "), js_string!("error"), 0)
.build();
context.register_global_property(js_string!("console"), console, Attribute::all())
}