use boa_engine::{Context, JsString, JsValue, object::builtins::JsArray};
use serde_json::{Map, Value};
use crate::error::JsLanguageError;
const MAX_DEPTH: u32 = 128;
const JS_MAX_SAFE_INT_I64: i64 = (1_i64 << 53) - 1;
const JS_MAX_SAFE_INT_F64: f64 = 9_007_199_254_740_991.0;
pub fn value_to_js(value: &Value, ctx: &mut Context) -> Result<JsValue, JsLanguageError> {
value_to_js_inner(value, ctx, 0)
}
fn value_to_js_inner(
value: &Value,
ctx: &mut Context,
depth: u32,
) -> Result<JsValue, JsLanguageError> {
if depth > MAX_DEPTH {
return Err(JsLanguageError::TypeConversion {
message: format!("value nesting exceeds maximum depth of {MAX_DEPTH}"),
});
}
match value {
Value::Null => Ok(JsValue::null()),
Value::Bool(b) => Ok(JsValue::Boolean(*b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
Ok(JsValue::Integer(i as i32))
} else if (-JS_MAX_SAFE_INT_I64..=JS_MAX_SAFE_INT_I64).contains(&i) {
Ok(JsValue::Rational(i as f64))
} else {
Err(JsLanguageError::TypeConversion {
message: format!(
"integer {i} is beyond JS safe integer range (±2^53-1); use a string or BigInt instead"
),
})
}
} else if let Some(f) = n.as_f64() {
Ok(JsValue::Rational(f))
} else {
Err(JsLanguageError::TypeConversion {
message: format!("cannot convert number: {n}"),
})
}
}
Value::String(s) => Ok(JsValue::String(JsString::from(s.as_str()))),
Value::Array(arr) => {
let js_arr = JsArray::new(ctx);
for (i, item) in arr.iter().enumerate() {
let js_item = value_to_js_inner(item, ctx, depth + 1)?;
js_arr.set(i as u32, js_item, false, ctx).map_err(|e| {
JsLanguageError::TypeConversion {
message: format!("array set error at {i}: {e}"),
}
})?;
}
Ok(js_arr.into())
}
Value::Object(map) => {
let obj = boa_engine::object::ObjectInitializer::new(ctx).build();
for (k, v) in map {
let js_val = value_to_js_inner(v, ctx, depth + 1)?;
obj.set(boa_engine::JsString::from(k.as_str()), js_val, false, ctx)
.map_err(|e| JsLanguageError::TypeConversion {
message: format!("object set error for key '{k}': {e}"),
})?;
}
Ok(obj.into())
}
}
}
pub fn js_to_value(value: &JsValue, ctx: &mut Context) -> Result<Value, JsLanguageError> {
js_to_value_inner(value, ctx, 0)
}
fn js_to_value_inner(
value: &JsValue,
ctx: &mut Context,
depth: u32,
) -> Result<Value, JsLanguageError> {
if depth > MAX_DEPTH {
return Err(JsLanguageError::TypeConversion {
message: format!("JS value nesting exceeds maximum depth of {MAX_DEPTH}"),
});
}
match value {
JsValue::Null | JsValue::Undefined => Ok(Value::Null),
JsValue::Boolean(b) => Ok(Value::Bool(*b)),
JsValue::Integer(i) => Ok(Value::Number((*i).into())),
JsValue::Rational(f) => {
if f.fract() == 0.0 && *f >= -JS_MAX_SAFE_INT_F64 && *f <= JS_MAX_SAFE_INT_F64 {
return Ok(Value::Number((*f as i64).into()));
}
let n = serde_json::Number::from_f64(*f).ok_or_else(|| {
JsLanguageError::TypeConversion {
message: format!("non-finite float: {f}"),
}
})?;
Ok(Value::Number(n))
}
JsValue::String(s) => Ok(Value::String(s.to_std_string_escaped())),
JsValue::Object(obj) => {
if obj.is_array() {
let js_arr = JsArray::from_object(obj.clone()).map_err(|e| {
JsLanguageError::TypeConversion {
message: format!("array conversion error: {e}"),
}
})?;
let len = js_arr
.length(ctx)
.map_err(|e| JsLanguageError::TypeConversion {
message: format!("array length error: {e}"),
})?;
let mut arr = Vec::with_capacity(len as usize);
for i in 0..len {
let item = js_arr
.get(i, ctx)
.map_err(|e| JsLanguageError::TypeConversion {
message: format!("array get error at {i}: {e}"),
})?;
arr.push(js_to_value_inner(&item, ctx, depth + 1)?);
}
Ok(Value::Array(arr))
} else {
let keys =
obj.own_property_keys(ctx)
.map_err(|e| JsLanguageError::TypeConversion {
message: format!("object keys error: {e}"),
})?;
let mut map = Map::new();
for key in keys {
let key_str = match &key {
boa_engine::property::PropertyKey::String(s) => s.to_std_string_escaped(),
boa_engine::property::PropertyKey::Index(i) => i.get().to_string(),
boa_engine::property::PropertyKey::Symbol(_) => continue, };
let val = obj
.get(boa_engine::JsString::from(key_str.as_str()), ctx)
.map_err(|e| JsLanguageError::TypeConversion {
message: format!("object get error for key '{key_str}': {e}"),
})?;
map.insert(key_str, js_to_value_inner(&val, ctx, depth + 1)?);
}
Ok(Value::Object(map))
}
}
JsValue::Symbol(_) => Err(JsLanguageError::TypeConversion {
message: "JS Symbol cannot be converted to serde_json::Value".to_string(),
}),
JsValue::BigInt(_) => Err(JsLanguageError::TypeConversion {
message: "JS BigInt cannot be converted to serde_json::Value".to_string(),
}),
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
use serde_json::json;
fn ctx() -> Context {
Context::default()
}
#[test]
fn test_null_roundtrip() {
let mut ctx = ctx();
let js = value_to_js(&Value::Null, &mut ctx).unwrap();
assert!(js.is_null());
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back, Value::Null);
}
#[test]
fn test_bool_roundtrip() {
let mut ctx = ctx();
for b in [true, false] {
let v = json!(b);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back, v);
}
}
#[test]
fn test_integer_roundtrip() {
let mut ctx = ctx();
let v = json!(42);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back.as_i64().unwrap(), 42);
}
#[test]
fn test_large_integer_no_truncation() {
let mut ctx = ctx();
let v = json!(2147483648i64); let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back.as_i64().unwrap(), 2147483648i64);
}
#[test]
fn test_negative_integer() {
let mut ctx = ctx();
let v = json!(-42);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back.as_i64().unwrap(), -42);
}
#[test]
fn test_float_roundtrip() {
let mut ctx = ctx();
let v = json!(PI);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert!((back.as_f64().unwrap() - PI).abs() < 1e-10);
}
#[test]
fn test_string_roundtrip() {
let mut ctx = ctx();
let v = json!("hello world");
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back, v);
}
#[test]
fn test_array_roundtrip() {
let mut ctx = ctx();
let v = json!([1, "two", true, null]);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back[0].as_i64().unwrap(), 1);
assert_eq!(back[1].as_str().unwrap(), "two");
assert!(back[2].as_bool().unwrap());
assert!(back[3].is_null());
}
#[test]
fn test_empty_array() {
let mut ctx = ctx();
let v = json!([]);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert!(back.as_array().unwrap().is_empty());
}
#[test]
fn test_object_roundtrip() {
let mut ctx = ctx();
let v = json!({"name": "test", "count": 3, "active": true});
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back["name"].as_str().unwrap(), "test");
assert_eq!(back["count"].as_i64().unwrap(), 3);
assert!(back["active"].as_bool().unwrap());
}
#[test]
fn test_empty_object() {
let mut ctx = ctx();
let v = json!({});
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert!(back.as_object().unwrap().is_empty());
}
#[test]
fn test_nested_object_roundtrip() {
let mut ctx = ctx();
let v = json!({"outer": {"inner": [1, 2, 3]}});
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back["outer"]["inner"][1].as_i64().unwrap(), 2);
}
#[test]
fn test_depth_limit() {
let mut ctx = ctx();
let mut v = json!("leaf");
for _ in 0..200 {
v = json!({"inner": v});
}
let result = value_to_js(&v, &mut ctx);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("depth"));
}
#[test]
fn test_beyond_safe_integer_errors() {
let mut ctx = ctx();
let v = json!(9007199254740993i64);
let result = value_to_js(&v, &mut ctx);
assert!(result.is_err(), "should error for beyond-safe-integer");
assert!(result.unwrap_err().to_string().contains("safe integer"));
}
#[test]
fn test_safe_integer_boundary() {
let mut ctx = ctx();
let v = json!(9007199254740991i64);
let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back.as_i64().unwrap(), 9007199254740991i64);
}
#[test]
fn test_safe_integer_negative_boundary() {
let mut ctx = ctx();
let v = json!(-9007199254740991i64); let js = value_to_js(&v, &mut ctx).unwrap();
let back = js_to_value(&js, &mut ctx).unwrap();
assert_eq!(back.as_i64().unwrap(), -9007199254740991i64);
}
#[test]
fn test_beyond_safe_integer_negative_errors() {
let mut ctx = ctx();
let v = json!(-9007199254740993i64); let result = value_to_js(&v, &mut ctx);
assert!(
result.is_err(),
"should error for negative beyond-safe-integer"
);
assert!(result.unwrap_err().to_string().contains("safe integer"));
}
}