use boa_engine::{
Context, JsError, JsNativeError, JsString, JsValue, js_string, object::ObjectInitializer,
property::Attribute,
};
pub trait IntoJs {
fn into_js(self, ctx: &mut Context) -> Result<JsValue, JsError>;
}
pub trait FromJs: Sized {
fn from_js(value: &JsValue, ctx: &mut Context) -> Result<Self, JsError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostError {
kind: HostErrorKind,
message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HostErrorKind {
Error,
TypeError,
RangeError,
}
impl HostError {
pub fn new(message: impl Into<String>) -> Self {
Self {
kind: HostErrorKind::Error,
message: message.into(),
}
}
pub fn type_error(message: impl Into<String>) -> Self {
Self {
kind: HostErrorKind::TypeError,
message: message.into(),
}
}
pub fn range_error(message: impl Into<String>) -> Self {
Self {
kind: HostErrorKind::RangeError,
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for HostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for HostError {}
impl From<String> for HostError {
fn from(message: String) -> Self {
Self::new(message)
}
}
impl From<&str> for HostError {
fn from(message: &str) -> Self {
Self::new(message.to_owned())
}
}
impl From<JsError> for HostError {
fn from(e: JsError) -> Self {
Self {
kind: HostErrorKind::Error,
message: e.to_string(),
}
}
}
impl IntoJsError for HostError {
fn into_js_error(self) -> JsError {
let native = match self.kind {
HostErrorKind::Error => JsNativeError::error(),
HostErrorKind::TypeError => JsNativeError::typ(),
HostErrorKind::RangeError => JsNativeError::range(),
};
JsError::from_native(native.with_message(self.message))
}
}
pub trait IntoJsError {
fn into_js_error(self) -> JsError;
}
impl IntoJsError for String {
fn into_js_error(self) -> JsError {
JsError::from_native(JsNativeError::error().with_message(self))
}
}
impl IntoJsError for &str {
fn into_js_error(self) -> JsError {
JsError::from_native(JsNativeError::error().with_message(self.to_owned()))
}
}
fn type_err(msg: impl Into<String>) -> JsError {
JsError::from_native(JsNativeError::typ().with_message(msg.into()))
}
impl IntoJs for String {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(js_string!(self).into())
}
}
impl IntoJs for &str {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(js_string!(self).into())
}
}
impl IntoJs for JsString {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(self.into())
}
}
impl IntoJs for f64 {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(JsValue::from(self))
}
}
impl IntoJs for f32 {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(JsValue::from(self as f64))
}
}
macro_rules! int_into_js {
($($t:ty),*) => {
$(
impl IntoJs for $t {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(JsValue::from(self))
}
}
)*
};
}
int_into_js!(i8, i16, i32, i64, u8, u16, u32, u64, isize, usize);
impl IntoJs for bool {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(JsValue::from(self))
}
}
impl IntoJs for () {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(JsValue::undefined())
}
}
impl IntoJs for JsValue {
fn into_js(self, _ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(self)
}
}
impl<T: IntoJs> IntoJs for Option<T> {
fn into_js(self, ctx: &mut Context) -> Result<JsValue, JsError> {
match self {
Some(v) => v.into_js(ctx),
None => Ok(JsValue::null()),
}
}
}
impl<T: IntoJs> IntoJs for Vec<T> {
fn into_js(self, ctx: &mut Context) -> Result<JsValue, JsError> {
let arr = boa_engine::object::builtins::JsArray::new(ctx);
for item in self {
arr.push(item.into_js(ctx)?, ctx)?;
}
Ok(arr.into())
}
}
impl<T: IntoJs, E: IntoJsError> IntoJs for Result<T, E> {
fn into_js(self, ctx: &mut Context) -> Result<JsValue, JsError> {
match self {
Ok(v) => v.into_js(ctx),
Err(e) => Err(e.into_js_error()),
}
}
}
impl FromJs for String {
fn from_js(value: &JsValue, ctx: &mut Context) -> Result<Self, JsError> {
value
.to_string(ctx)
.map(|s| s.to_std_string_lossy())
.map_err(|_| type_err("expected a string"))
}
}
impl FromJs for f64 {
fn from_js(value: &JsValue, ctx: &mut Context) -> Result<Self, JsError> {
value
.to_number(ctx)
.map_err(|_| type_err("expected a number"))
}
}
macro_rules! num_from_js {
($($t:ty),*) => {
$(
impl FromJs for $t {
fn from_js(value: &JsValue, ctx: &mut Context) -> Result<Self, JsError> {
let n = f64::from_js(value, ctx)?;
Ok(n as $t)
}
}
)*
};
}
num_from_js!(f32, i8, i16, i32, i64, u8, u16, u32, u64, isize, usize);
impl FromJs for bool {
fn from_js(value: &JsValue, _ctx: &mut Context) -> Result<Self, JsError> {
value
.as_boolean()
.ok_or_else(|| type_err("expected a boolean"))
}
}
impl FromJs for () {
fn from_js(_value: &JsValue, _ctx: &mut Context) -> Result<Self, JsError> {
Ok(())
}
}
impl FromJs for JsValue {
fn from_js(value: &JsValue, _ctx: &mut Context) -> Result<Self, JsError> {
Ok(value.clone())
}
}
impl<T: FromJs> FromJs for Option<T> {
fn from_js(value: &JsValue, ctx: &mut Context) -> Result<Self, JsError> {
if value.is_undefined() || value.is_null() {
return Ok(None);
}
T::from_js(value, ctx).map(Some)
}
}
impl<T: FromJs> FromJs for Vec<T> {
fn from_js(value: &JsValue, ctx: &mut Context) -> Result<Self, JsError> {
let arr = value
.as_object()
.ok_or_else(|| type_err("expected an array"))?;
let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
let mut out = Vec::with_capacity(len as usize);
for i in 0..len {
let item = arr.get(i, ctx)?;
out.push(T::from_js(&item, ctx)?);
}
Ok(out)
}
}
type LazyProp = Box<dyn FnOnce(&mut Context) -> JsValue>;
pub struct JsObjectBuilder {
props: Vec<(String, LazyProp)>,
}
impl JsObjectBuilder {
pub fn new() -> Self {
Self { props: Vec::new() }
}
pub fn set<V: IntoJs + 'static>(&mut self, key: impl Into<String>, val: V) {
let key = key.into();
self.props.push((
key.clone(),
Box::new(move |ctx| match val.into_js(ctx) {
Ok(v) => v,
Err(e) => {
log::warn!("JsObjectBuilder: property `{key}` failed to convert: {e}");
JsValue::undefined()
}
}),
));
}
pub fn build(self, ctx: &mut Context) -> JsValue {
let props: Vec<(String, JsValue)> = self
.props
.into_iter()
.map(|(key, conv)| (key, conv(ctx)))
.collect();
let mut init = ObjectInitializer::new(ctx);
for (key, v) in props {
init.property(js_string!(key), v, Attribute::all());
}
init.build().into()
}
}
impl Default for JsObjectBuilder {
fn default() -> Self {
Self::new()
}
}
impl IntoJs for JsObjectBuilder {
fn into_js(self, ctx: &mut Context) -> Result<JsValue, JsError> {
Ok(self.build(ctx))
}
}