use std::collections::HashMap;
use boa_engine::{
Context, JsValue, js_string,
native_function::NativeFunction,
object::{JsObject, builtins::JsArray},
property::PropertyKey,
};
use serde_json::Value;
use crate::{
engine::JsExchange,
error::JsLanguageError,
value::{js_to_value, value_to_js},
};
macro_rules! make_console_fn {
($level:ident, $ctx:expr) => {{
let f = NativeFunction::from_copy_closure(move |_this, args, ctx| {
let msg = args
.iter()
.map(|a| {
a.to_string(ctx)
.map(|s| s.to_std_string_escaped())
.unwrap_or_default()
})
.collect::<Vec<_>>()
.join(" ");
tracing::$level!(target: "camel_language_js::console", "{}", msg);
Ok(JsValue::undefined())
});
f.to_js_function($ctx.realm())
}};
}
pub fn register_console(ctx: &mut Context) {
let console = JsObject::with_null_proto();
let log_js = make_console_fn!(info, ctx);
let _ = console.set(js_string!("log"), JsValue::from(log_js), false, ctx);
let warn_js = make_console_fn!(warn, ctx);
let _ = console.set(js_string!("warn"), JsValue::from(warn_js), false, ctx);
let error_js = make_console_fn!(error, ctx);
let _ = console.set(js_string!("error"), JsValue::from(error_js), false, ctx);
let debug_js = make_console_fn!(debug, ctx);
let _ = console.set(js_string!("debug"), JsValue::from(debug_js), false, ctx);
let _ = ctx
.global_object()
.set(js_string!("console"), JsValue::from(console), false, ctx);
}
pub fn build_camel_global(
exchange: &JsExchange,
ctx: &mut Context,
) -> Result<JsObject, JsLanguageError> {
let camel = JsObject::with_null_proto();
let headers = build_map_object(&exchange.headers, ctx)?;
camel
.set(js_string!("headers"), JsValue::from(headers), false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let properties = build_map_object(&exchange.properties, ctx)?;
camel
.set(
js_string!("properties"),
JsValue::from(properties.clone()),
false,
ctx,
)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let properties_get = properties.clone();
let property_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, args, props, ctx| {
let key = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(ctx)?
.to_std_string_escaped();
let data = props.get(js_string!("__data"), ctx)?;
let data_obj = data.as_object().ok_or_else(|| {
boa_engine::JsNativeError::typ().with_message("properties.__data missing")
})?;
data_obj.get(js_string!(key.as_str()), ctx)
},
properties_get,
);
let property_js = property_fn.to_js_function(ctx.realm());
camel
.set(
js_string!("property"),
JsValue::from(property_js),
false,
ctx,
)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let properties_set = properties.clone();
let set_property_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, args, props, ctx| {
let key = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(ctx)?
.to_std_string_escaped();
let val = args.get(1).cloned().unwrap_or(JsValue::undefined());
let data = props.get(js_string!("__data"), ctx)?;
let data_obj = data.as_object().ok_or_else(|| {
boa_engine::JsNativeError::typ().with_message("properties.__data missing")
})?;
data_obj.set(js_string!(key.as_str()), val, false, ctx)?;
Ok(JsValue::undefined())
},
properties_set,
);
let set_property_js = set_property_fn.to_js_function(ctx.realm());
camel
.set(
js_string!("set_property"),
JsValue::from(set_property_js),
false,
ctx,
)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let body_val = value_to_js(&exchange.body, ctx)?;
camel
.set(js_string!("body"), body_val, false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
Ok(camel)
}
fn build_map_object(
map: &HashMap<String, Value>,
ctx: &mut Context,
) -> Result<JsObject, JsLanguageError> {
let data = JsObject::with_null_proto();
for (k, v) in map {
let js_val = value_to_js(v, ctx)?;
data.set(js_string!(k.as_str()), js_val, false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
}
let map_obj = JsObject::with_null_proto();
map_obj
.set(
js_string!("__data"),
JsValue::from(data.clone()),
false,
ctx,
)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let data_get = data.clone();
let get_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, args, data_obj, ctx| {
let key = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(ctx)?
.to_std_string_escaped();
data_obj.get(js_string!(key.as_str()), ctx)
},
data_get,
);
let get_js = get_fn.to_js_function(ctx.realm());
map_obj
.set(js_string!("get"), JsValue::from(get_js), false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let data_set = data.clone();
let set_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, args, data_obj, ctx| {
let key = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(ctx)?
.to_std_string_escaped();
let val = args.get(1).cloned().unwrap_or(JsValue::undefined());
data_obj.set(js_string!(key.as_str()), val, false, ctx)?;
Ok(JsValue::undefined())
},
data_set,
);
let set_js = set_fn.to_js_function(ctx.realm());
map_obj
.set(js_string!("set"), JsValue::from(set_js), false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let data_has = data.clone();
let has_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, args, data_obj, ctx| {
let key = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(ctx)?
.to_std_string_escaped();
let has = data_obj.has_own_property(js_string!(key.as_str()), ctx)?;
Ok(JsValue::from(has))
},
data_has,
);
let has_js = has_fn.to_js_function(ctx.realm());
map_obj
.set(js_string!("has"), JsValue::from(has_js), false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let data_remove = data.clone();
let remove_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, args, data_obj, ctx| {
let key = args
.first()
.unwrap_or(&JsValue::undefined())
.to_string(ctx)?
.to_std_string_escaped();
data_obj.delete_property_or_throw(js_string!(key.as_str()), ctx)?;
Ok(JsValue::undefined())
},
data_remove,
);
let remove_js = remove_fn.to_js_function(ctx.realm());
map_obj
.set(js_string!("remove"), JsValue::from(remove_js), false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
let data_keys = data.clone();
let keys_fn = NativeFunction::from_copy_closure_with_captures(
move |_this, _args, data_obj, ctx| {
let keys = data_obj.own_property_keys(ctx)?;
let js_keys: Vec<JsValue> = keys
.iter()
.filter_map(|k| match k {
PropertyKey::String(s) => Some(JsValue::from(s.clone())),
_ => None,
})
.collect();
let arr = JsArray::from_iter(js_keys, ctx);
Ok(JsValue::from(arr))
},
data_keys,
);
let keys_js = keys_fn.to_js_function(ctx.realm());
map_obj
.set(js_string!("keys"), JsValue::from(keys_js), false, ctx)
.map_err(|e| JsLanguageError::Execution {
message: e.to_string(),
})?;
Ok(map_obj)
}
pub fn extract_camel_state(ctx: &mut Context) -> Result<JsExchange, JsLanguageError> {
let camel_val = ctx
.global_object()
.get(js_string!("camel"), ctx)
.map_err(|e| JsLanguageError::ExchangeAccess {
message: e.to_string(),
})?;
let camel = match camel_val.as_object() {
Some(obj) => obj,
None => {
return Err(JsLanguageError::ExchangeAccess {
message: "camel global is not an object".to_string(),
});
}
};
let headers = extract_map(
&camel
.get(js_string!("headers"), ctx)
.map_err(|e| JsLanguageError::ExchangeAccess {
message: e.to_string(),
})?,
ctx,
)?;
let properties = extract_map(
&camel
.get(js_string!("properties"), ctx)
.map_err(|e| JsLanguageError::ExchangeAccess {
message: e.to_string(),
})?,
ctx,
)?;
let body_js =
camel
.get(js_string!("body"), ctx)
.map_err(|e| JsLanguageError::ExchangeAccess {
message: e.to_string(),
})?;
let body = js_to_value(&body_js, ctx)?;
Ok(JsExchange {
headers,
body,
properties,
})
}
fn extract_map(
map_val: &JsValue,
ctx: &mut Context,
) -> Result<HashMap<String, Value>, JsLanguageError> {
let map_obj = match map_val.as_object() {
Some(obj) => obj,
None => return Ok(HashMap::new()),
};
let data_val =
map_obj
.get(js_string!("__data"), ctx)
.map_err(|e| JsLanguageError::ExchangeAccess {
message: e.to_string(),
})?;
let data_obj = match data_val.as_object() {
Some(obj) => obj,
None => return Ok(HashMap::new()),
};
let keys = data_obj
.own_property_keys(ctx)
.map_err(|e| JsLanguageError::ExchangeAccess {
message: e.to_string(),
})?;
let mut result = HashMap::new();
for key in &keys {
if let PropertyKey::String(s) = key {
let k = s.to_std_string_escaped();
let v = data_obj.get(js_string!(k.as_str()), ctx).map_err(|e| {
JsLanguageError::ExchangeAccess {
message: e.to_string(),
}
})?;
let val = js_to_value(&v, ctx).map_err(|e| JsLanguageError::ExchangeAccess {
message: format!("value extraction failed for key '{k}': {e}"),
})?;
result.insert(k, val);
}
}
Ok(result)
}