use std::cell::RefCell;
use std::collections::HashSet;
use std::rc::Rc;
use crate::builtins::proxy::{proxy_get, proxy_get_own_property_descriptor, proxy_own_keys};
use crate::error::{StatorError, StatorResult};
use crate::objects::value::JsValue;
pub type ReviverFn<'a> = &'a dyn Fn(&str, JsonValue) -> StatorResult<Option<JsonValue>>;
pub type ToJsonFn<'a> = &'a dyn Fn(&str, &JsonValue) -> Option<JsonValue>;
const MAX_INDENT_SIZE: usize = 10;
const INTEGER_THRESHOLD: f64 = 1e15;
#[derive(Debug, Clone)]
pub enum JsonValue {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Rc<RefCell<Vec<JsonValue>>>),
Object(Rc<RefCell<Vec<(String, JsonValue)>>>),
}
impl JsonValue {
pub fn new_array() -> Self {
Self::Array(Rc::new(RefCell::new(Vec::new())))
}
pub fn new_object() -> Self {
Self::Object(Rc::new(RefCell::new(Vec::new())))
}
#[inline]
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[inline]
pub fn is_bool(&self) -> bool {
matches!(self, Self::Bool(_))
}
#[inline]
pub fn is_number(&self) -> bool {
matches!(self, Self::Number(_))
}
#[inline]
pub fn is_string(&self) -> bool {
matches!(self, Self::Str(_))
}
#[inline]
pub fn is_array(&self) -> bool {
matches!(self, Self::Array(_))
}
#[inline]
pub fn is_object(&self) -> bool {
matches!(self, Self::Object(_))
}
}
impl PartialEq for JsonValue {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Null, Self::Null) => true,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Number(a), Self::Number(b)) => {
a == b
}
(Self::Str(a), Self::Str(b)) => a == b,
(Self::Array(a), Self::Array(b)) => {
*a.borrow() == *b.borrow()
}
(Self::Object(a), Self::Object(b)) => {
*a.borrow() == *b.borrow()
}
_ => false,
}
}
}
pub enum JsonReplacer<'a> {
Function(&'a dyn Fn(&str, &JsonValue) -> StatorResult<Option<JsonValue>>),
Array(Vec<String>),
}
pub enum JsonSpace {
Count(u32),
Str(String),
}
pub fn json_parse(text: &str, reviver: Option<ReviverFn<'_>>) -> StatorResult<JsonValue> {
let chars: Vec<char> = text.chars().collect();
let mut parser = Parser {
src: &chars,
pos: 0,
depth: 0,
};
parser.skip_ws();
let value = parser.parse_value()?;
parser.skip_ws();
if parser.pos != parser.src.len() {
return Err(StatorError::SyntaxError(format!(
"Unexpected token at position {}",
parser.pos
)));
}
if let Some(rev) = reviver {
Ok(apply_reviver(value, "", rev)?.unwrap_or(JsonValue::Null))
} else {
Ok(value)
}
}
pub fn json_stringify(
value: &JsonValue,
replacer: Option<&JsonReplacer<'_>>,
space: Option<&JsonSpace>,
to_json: Option<ToJsonFn<'_>>,
) -> StatorResult<Option<String>> {
let indent = resolve_indent(space);
let mut in_progress: HashSet<usize> = HashSet::new();
stringify_value(value, "", replacer, &indent, 0, &mut in_progress, to_json)
}
pub fn json_stringify_js_value(
value: &JsValue,
replacer: Option<&JsonReplacer<'_>>,
space: Option<&JsonSpace>,
) -> StatorResult<Option<String>> {
let json = js_value_to_json(value)?;
match json {
Some(jv) => json_stringify(&jv, replacer, space, None),
None => Ok(None),
}
}
const JSON_MAX_DEPTH: usize = 512;
struct Parser<'a> {
src: &'a [char],
pos: usize,
depth: usize,
}
impl<'a> Parser<'a> {
fn skip_ws(&mut self) {
while self.pos < self.src.len() && matches!(self.src[self.pos], ' ' | '\t' | '\r' | '\n') {
self.pos += 1;
}
}
fn peek(&self) -> Option<char> {
self.src.get(self.pos).copied()
}
fn advance(&mut self) -> Option<char> {
let ch = self.src.get(self.pos).copied();
if ch.is_some() {
self.pos += 1;
}
ch
}
fn expect(&mut self, expected: char) -> StatorResult<()> {
match self.advance() {
Some(c) if c == expected => Ok(()),
Some(c) => Err(StatorError::SyntaxError(format!(
"Expected '{expected}', got '{c}' at position {}",
self.pos - 1
))),
None => Err(StatorError::SyntaxError(format!(
"Expected '{expected}' but reached end of input"
))),
}
}
fn expect_literal(&mut self, lit: &str) -> StatorResult<()> {
for expected in lit.chars() {
match self.advance() {
Some(c) if c == expected => {}
Some(c) => {
return Err(StatorError::SyntaxError(format!(
"Expected '{lit}', found unexpected '{c}' at position {}",
self.pos - 1
)));
}
None => {
return Err(StatorError::SyntaxError(format!(
"Expected '{lit}', reached end of input"
)));
}
}
}
Ok(())
}
fn parse_value(&mut self) -> StatorResult<JsonValue> {
self.skip_ws();
match self.peek() {
Some('"') => self.parse_string().map(JsonValue::Str),
Some('{') | Some('[') => {
self.depth += 1;
if self.depth > JSON_MAX_DEPTH {
return Err(StatorError::SyntaxError(
"JSON nesting depth exceeded".to_string(),
));
}
let result = if self.peek() == Some('{') {
self.parse_object()
} else {
self.parse_array()
};
self.depth -= 1;
result
}
Some('t') => {
self.expect_literal("true")?;
Ok(JsonValue::Bool(true))
}
Some('f') => {
self.expect_literal("false")?;
Ok(JsonValue::Bool(false))
}
Some('n') => {
self.expect_literal("null")?;
Ok(JsonValue::Null)
}
Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
Some(c) => Err(StatorError::SyntaxError(format!(
"Unexpected character '{c}' at position {}",
self.pos
))),
None => Err(StatorError::SyntaxError(
"Unexpected end of JSON input".to_string(),
)),
}
}
fn parse_string(&mut self) -> StatorResult<String> {
self.expect('"')?;
let mut s = String::new();
loop {
match self.advance() {
None => return Err(StatorError::SyntaxError("Unterminated string".to_string())),
Some('"') => break,
Some('\\') => {
let escaped = self.parse_escape()?;
s.push_str(&escaped);
}
Some(c) if (c as u32) < 0x20 => {
return Err(StatorError::SyntaxError(format!(
"Unescaped control character U+{:04X} in string",
c as u32
)));
}
Some(c) => s.push(c),
}
}
Ok(s)
}
fn parse_escape(&mut self) -> StatorResult<String> {
match self.advance() {
Some('"') => Ok("\"".to_string()),
Some('\\') => Ok("\\".to_string()),
Some('/') => Ok("/".to_string()),
Some('b') => Ok("\x08".to_string()),
Some('f') => Ok("\x0C".to_string()),
Some('n') => Ok("\n".to_string()),
Some('r') => Ok("\r".to_string()),
Some('t') => Ok("\t".to_string()),
Some('u') => {
let cp = self.parse_hex4()?;
if (0xD800..=0xDBFF).contains(&cp) {
if self.peek() == Some('\\') {
self.advance(); if self.peek() == Some('u') {
self.advance(); let low = self.parse_hex4()?;
if (0xDC00..=0xDFFF).contains(&low) {
let scalar =
0x10000 + ((cp as u32 - 0xD800) << 10) + (low as u32 - 0xDC00);
let ch = char::from_u32(scalar).ok_or_else(|| {
StatorError::SyntaxError(format!(
"Invalid surrogate pair U+{cp:04X} U+{low:04X}"
))
})?;
return Ok(ch.to_string());
}
let high_ch =
char::from_u32(cp as u32).unwrap_or(char::REPLACEMENT_CHARACTER);
let low_ch =
char::from_u32(low as u32).unwrap_or(char::REPLACEMENT_CHARACTER);
return Ok(format!("{high_ch}{low_ch}"));
}
self.pos -= 1;
}
return Ok(char::REPLACEMENT_CHARACTER.to_string());
}
let ch = char::from_u32(cp as u32).unwrap_or(char::REPLACEMENT_CHARACTER);
Ok(ch.to_string())
}
Some(c) => Err(StatorError::SyntaxError(format!(
"Invalid escape sequence '\\{c}'"
))),
None => Err(StatorError::SyntaxError(
"Unexpected end of input in escape sequence".to_string(),
)),
}
}
fn parse_hex4(&mut self) -> StatorResult<u16> {
let mut val: u32 = 0;
for _ in 0..4 {
match self.advance() {
Some(c) if c.is_ascii_hexdigit() => {
val = val * 16 + c.to_digit(16).unwrap();
}
Some(c) => {
return Err(StatorError::SyntaxError(format!(
"Invalid hex digit '{c}' in \\uXXXX escape"
)));
}
None => {
return Err(StatorError::SyntaxError(
"Unexpected end of input in \\uXXXX escape".to_string(),
));
}
}
}
Ok(val as u16)
}
fn parse_number(&mut self) -> StatorResult<JsonValue> {
let start = self.pos;
if self.peek() == Some('-') {
self.advance();
}
match self.peek() {
Some('0') => {
self.advance();
}
Some(c) if c.is_ascii_digit() => {
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.advance();
}
}
_ => {
return Err(StatorError::SyntaxError(format!(
"Invalid number at position {start}"
)));
}
}
if self.peek() == Some('.') {
self.advance();
if !self.peek().is_some_and(|c| c.is_ascii_digit()) {
return Err(StatorError::SyntaxError(
"Expected digit after decimal point".to_string(),
));
}
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.advance();
}
}
if matches!(self.peek(), Some('e') | Some('E')) {
self.advance();
if matches!(self.peek(), Some('+') | Some('-')) {
self.advance();
}
if !self.peek().is_some_and(|c| c.is_ascii_digit()) {
return Err(StatorError::SyntaxError(
"Expected digit in exponent".to_string(),
));
}
while self.peek().is_some_and(|c| c.is_ascii_digit()) {
self.advance();
}
}
let slice: String = self.src[start..self.pos].iter().collect();
let n: f64 = slice
.parse()
.map_err(|_| StatorError::SyntaxError(format!("Invalid number literal '{slice}'")))?;
Ok(JsonValue::Number(n))
}
fn parse_array(&mut self) -> StatorResult<JsonValue> {
self.expect('[')?;
let arr = Rc::new(RefCell::new(Vec::new()));
self.skip_ws();
if self.peek() == Some(']') {
self.advance();
return Ok(JsonValue::Array(arr));
}
loop {
self.skip_ws();
let v = self.parse_value()?;
arr.borrow_mut().push(v);
self.skip_ws();
match self.peek() {
Some(',') => {
self.advance();
}
Some(']') => {
self.advance();
break;
}
Some(c) => {
return Err(StatorError::SyntaxError(format!(
"Expected ',' or ']', got '{c}' at position {}",
self.pos
)));
}
None => {
return Err(StatorError::SyntaxError("Unterminated array".to_string()));
}
}
}
Ok(JsonValue::Array(arr))
}
fn parse_object(&mut self) -> StatorResult<JsonValue> {
self.expect('{')?;
let obj = Rc::new(RefCell::new(Vec::new()));
self.skip_ws();
if self.peek() == Some('}') {
self.advance();
return Ok(JsonValue::Object(obj));
}
loop {
self.skip_ws();
if self.peek() != Some('"') {
return Err(StatorError::SyntaxError(format!(
"Expected string key at position {}",
self.pos
)));
}
let key = self.parse_string()?;
self.skip_ws();
self.expect(':')?;
self.skip_ws();
let val = self.parse_value()?;
let mut entries = obj.borrow_mut();
if let Some(existing) = entries.iter_mut().find(|(k, _)| k == &key) {
existing.1 = val;
} else {
entries.push((key, val));
}
drop(entries);
self.skip_ws();
match self.peek() {
Some(',') => {
self.advance();
}
Some('}') => {
self.advance();
break;
}
Some(c) => {
return Err(StatorError::SyntaxError(format!(
"Expected ',' or '}}', got '{c}' at position {}",
self.pos
)));
}
None => {
return Err(StatorError::SyntaxError("Unterminated object".to_string()));
}
}
}
Ok(JsonValue::Object(obj))
}
}
fn apply_reviver(
value: JsonValue,
key: &str,
reviver: &dyn Fn(&str, JsonValue) -> StatorResult<Option<JsonValue>>,
) -> StatorResult<Option<JsonValue>> {
let transformed = match value {
JsonValue::Array(ref arr) => {
let items: Vec<JsonValue> = {
let borrow = arr.borrow();
borrow.clone()
};
let mut new_items = Vec::with_capacity(items.len());
for (i, item) in items.into_iter().enumerate() {
let idx_str = i.to_string();
let revived = apply_reviver(item, &idx_str, reviver)?;
new_items.push(revived.unwrap_or(JsonValue::Null));
}
*arr.borrow_mut() = new_items;
value
}
JsonValue::Object(ref obj) => {
let pairs: Vec<(String, JsonValue)> = {
let borrow = obj.borrow();
borrow.clone()
};
let mut new_pairs = Vec::with_capacity(pairs.len());
for (k, v) in pairs {
let revived = apply_reviver(v, &k, reviver)?;
if let Some(rv) = revived {
new_pairs.push((k, rv));
}
}
*obj.borrow_mut() = new_pairs;
value
}
other => other,
};
reviver(key, transformed)
}
fn resolve_indent(space: Option<&JsonSpace>) -> String {
match space {
None => String::new(),
Some(JsonSpace::Count(n)) => {
let count = (*n).min(MAX_INDENT_SIZE as u32) as usize;
" ".repeat(count)
}
Some(JsonSpace::Str(s)) => {
s.chars().take(MAX_INDENT_SIZE).collect()
}
}
}
#[allow(clippy::too_many_arguments)]
fn stringify_value(
value: &JsonValue,
key: &str,
replacer: Option<&JsonReplacer<'_>>,
indent: &str,
depth: usize,
in_progress: &mut HashSet<usize>,
to_json: Option<ToJsonFn<'_>>,
) -> StatorResult<Option<String>> {
let to_json_owned;
let value = if let Some(hook) = to_json {
if let Some(replacement) = hook(key, value) {
to_json_owned = replacement;
&to_json_owned
} else {
value
}
} else {
value
};
let fn_owned;
let value = if let Some(JsonReplacer::Function(f)) = replacer {
match f(key, value)? {
Some(replaced) => {
fn_owned = replaced;
&fn_owned
}
None => return Ok(None),
}
} else {
value
};
match value {
JsonValue::Null => Ok(Some("null".to_string())),
JsonValue::Bool(b) => Ok(Some(if *b { "true" } else { "false" }.to_string())),
JsonValue::Number(n) => {
if n.is_nan() || n.is_infinite() {
Ok(Some("null".to_string()))
} else {
if n.fract() == 0.0 && n.abs() < INTEGER_THRESHOLD {
Ok(Some(format!("{}", *n as i64)))
} else {
Ok(Some(format!("{n}")))
}
}
}
JsonValue::Str(s) => Ok(Some(stringify_string(s))),
JsonValue::Array(arr) => {
let ptr = Rc::as_ptr(arr) as usize;
if in_progress.contains(&ptr) {
return Err(StatorError::TypeError(
"Converting circular structure to JSON".to_string(),
));
}
in_progress.insert(ptr);
let result = stringify_array(arr, replacer, indent, depth, in_progress, to_json);
in_progress.remove(&ptr);
result
}
JsonValue::Object(obj) => {
let ptr = Rc::as_ptr(obj) as usize;
if in_progress.contains(&ptr) {
return Err(StatorError::TypeError(
"Converting circular structure to JSON".to_string(),
));
}
in_progress.insert(ptr);
let result = stringify_object(obj, replacer, indent, depth, in_progress, to_json);
in_progress.remove(&ptr);
result
}
}
}
fn stringify_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\x08' => out.push_str("\\b"),
'\x0C' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04X}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
fn stringify_array(
arr: &Rc<RefCell<Vec<JsonValue>>>,
replacer: Option<&JsonReplacer<'_>>,
indent: &str,
depth: usize,
in_progress: &mut HashSet<usize>,
to_json: Option<ToJsonFn<'_>>,
) -> StatorResult<Option<String>> {
let borrow = arr.borrow();
if borrow.is_empty() {
return Ok(Some("[]".to_string()));
}
let use_indent = !indent.is_empty();
let inner_indent = indent.repeat(depth + 1);
let outer_indent = indent.repeat(depth);
let mut parts = Vec::with_capacity(borrow.len());
for (i, item) in borrow.iter().enumerate() {
let idx_str = i.to_string();
let serialised = stringify_value(
item,
&idx_str,
replacer,
indent,
depth + 1,
in_progress,
to_json,
)?;
parts.push(serialised.unwrap_or_else(|| "null".to_string()));
}
if use_indent {
let joined = parts.join(&format!(",\n{inner_indent}"));
Ok(Some(format!("[\n{inner_indent}{joined}\n{outer_indent}]")))
} else {
Ok(Some(format!("[{}]", parts.join(","))))
}
}
fn stringify_object(
obj: &Rc<RefCell<Vec<(String, JsonValue)>>>,
replacer: Option<&JsonReplacer<'_>>,
indent: &str,
depth: usize,
in_progress: &mut HashSet<usize>,
to_json: Option<ToJsonFn<'_>>,
) -> StatorResult<Option<String>> {
let borrow = obj.borrow();
let use_indent = !indent.is_empty();
let inner_indent = indent.repeat(depth + 1);
let outer_indent = indent.repeat(depth);
let mut parts: Vec<String> = Vec::new();
for (k, v) in borrow.iter() {
if let Some(JsonReplacer::Array(allowed)) = replacer
&& !allowed.iter().any(|a| a == k)
{
continue;
}
let serialised = stringify_value(v, k, replacer, indent, depth + 1, in_progress, to_json)?;
if let Some(s) = serialised {
let key_str = stringify_string(k);
if use_indent {
parts.push(format!("{key_str}: {s}"));
} else {
parts.push(format!("{key_str}:{s}"));
}
}
}
if parts.is_empty() {
return Ok(Some("{}".to_string()));
}
if use_indent {
let joined = parts.join(&format!(",\n{inner_indent}"));
Ok(Some(format!(
"{{\n{inner_indent}{joined}\n{outer_indent}}}"
)))
} else {
Ok(Some(format!("{{{}}}", parts.join(","))))
}
}
pub fn js_value_to_json(value: &JsValue) -> StatorResult<Option<JsonValue>> {
js_value_to_json_inner(value, &mut HashSet::new())
}
fn js_value_to_json_inner(
value: &JsValue,
seen: &mut HashSet<usize>,
) -> StatorResult<Option<JsonValue>> {
match value {
JsValue::Undefined
| JsValue::TheHole
| JsValue::Symbol(_)
| JsValue::Function(_)
| JsValue::Generator(_)
| JsValue::Iterator(_)
| JsValue::Error(_)
| JsValue::Promise(_)
| JsValue::Context(_)
| JsValue::ArrayBuffer(_)
| JsValue::TypedArray(_)
| JsValue::DataView(_) => Ok(None),
JsValue::Null => Ok(Some(JsonValue::Null)),
JsValue::Boolean(b) => Ok(Some(JsonValue::Bool(*b))),
JsValue::Smi(n) => Ok(Some(JsonValue::Number(f64::from(*n)))),
JsValue::HeapNumber(n) => Ok(Some(JsonValue::Number(*n))),
JsValue::String(s) => Ok(Some(JsonValue::Str(s.to_string()))),
JsValue::BigInt(_) => Err(StatorError::TypeError(
"Do not know how to serialize a BigInt".to_string(),
)),
JsValue::Array(items) => {
let ptr = Rc::as_ptr(items) as usize;
if seen.contains(&ptr) {
return Err(StatorError::TypeError(
"Converting circular structure to JSON".to_string(),
));
}
seen.insert(ptr);
let mut arr: Vec<JsonValue> = Vec::with_capacity(items.borrow().len());
for item in items.borrow().iter() {
let json_item = js_value_to_json_inner(item, seen)?;
arr.push(json_item.unwrap_or(JsonValue::Null));
}
seen.remove(&ptr);
Ok(Some(JsonValue::Array(Rc::new(RefCell::new(arr)))))
}
JsValue::Object(_) => Ok(Some(JsonValue::Object(Rc::new(RefCell::new(Vec::new()))))),
JsValue::NativeFunction(_) => Ok(None),
JsValue::PlainObject(map) => {
let to_json_fn = map.borrow().get("toJSON").and_then(|v| {
if let JsValue::NativeFunction(f) = v {
Some(f.clone())
} else {
None
}
});
if let Some(f) = to_json_fn {
let result = f(vec![JsValue::String(String::new().into())])?;
return js_value_to_json_inner(&result, seen);
}
let ptr = Rc::as_ptr(map) as usize;
if seen.contains(&ptr) {
return Err(StatorError::TypeError(
"Converting circular structure to JSON".to_string(),
));
}
seen.insert(ptr);
let mut entries: Vec<(String, JsonValue)> = Vec::new();
for (k, v) in map.borrow().enumerable_iter() {
if let Some(jv) = js_value_to_json_inner(v, seen)? {
entries.push((k.to_string(), jv));
}
}
seen.remove(&ptr);
Ok(Some(JsonValue::Object(Rc::new(RefCell::new(entries)))))
}
JsValue::Proxy(proxy) => {
let ptr = Rc::as_ptr(proxy) as usize;
if seen.contains(&ptr) {
return Err(StatorError::TypeError(
"Converting circular structure to JSON".to_string(),
));
}
seen.insert(ptr);
let keys = proxy_own_keys(&proxy.borrow())?;
let mut entries: Vec<(String, JsonValue)> = Vec::new();
for key in keys {
let key_str = match &key {
JsValue::String(s) => s.to_string(),
_ => continue,
};
let Some((_, attrs)) =
proxy_get_own_property_descriptor(&proxy.borrow(), &key_str)?
else {
continue;
};
if !attrs.contains(crate::objects::map::PropertyAttributes::ENUMERABLE) {
continue;
}
let val = proxy_get(&proxy.borrow(), &key_str)?;
if let Some(jv) = js_value_to_json_inner(&val, seen)? {
entries.push((key_str, jv));
}
}
seen.remove(&ptr);
Ok(Some(JsonValue::Object(Rc::new(RefCell::new(entries)))))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_null() {
assert_eq!(json_parse("null", None).unwrap(), JsonValue::Null);
}
#[test]
fn test_parse_true() {
assert_eq!(json_parse("true", None).unwrap(), JsonValue::Bool(true));
}
#[test]
fn test_parse_false() {
assert_eq!(json_parse("false", None).unwrap(), JsonValue::Bool(false));
}
#[test]
fn test_parse_integer() {
assert_eq!(json_parse("42", None).unwrap(), JsonValue::Number(42.0));
}
#[test]
fn test_parse_negative_integer() {
assert_eq!(json_parse("-7", None).unwrap(), JsonValue::Number(-7.0));
}
#[test]
fn test_parse_float() {
assert_eq!(json_parse("3.14", None).unwrap(), JsonValue::Number(3.14));
}
#[test]
fn test_parse_exponent() {
assert_eq!(json_parse("1e2", None).unwrap(), JsonValue::Number(100.0));
}
#[test]
fn test_parse_string() {
assert_eq!(
json_parse(r#""hello""#, None).unwrap(),
JsonValue::Str("hello".to_string())
);
}
#[test]
fn test_parse_string_escape_sequences() {
let v = json_parse(r#""tab\there""#, None).unwrap();
assert_eq!(v, JsonValue::Str("tab\there".to_string()));
}
#[test]
fn test_parse_string_newline_escape() {
let v = json_parse(r#""line1\nline2""#, None).unwrap();
assert_eq!(v, JsonValue::Str("line1\nline2".to_string()));
}
#[test]
fn test_parse_unicode_escape_basic() {
let v = json_parse(r#""\u0041""#, None).unwrap();
assert_eq!(v, JsonValue::Str("A".to_string()));
}
#[test]
fn test_parse_unicode_escape_non_ascii() {
let v = json_parse(r#""\u00E9""#, None).unwrap();
assert_eq!(v, JsonValue::Str("é".to_string()));
}
#[test]
fn test_parse_unicode_surrogate_pair() {
let v = json_parse(r#""\uD83D\uDE00""#, None).unwrap();
assert_eq!(v, JsonValue::Str("😀".to_string()));
}
#[test]
fn test_parse_empty_array() {
let v = json_parse("[]", None).unwrap();
if let JsonValue::Array(arr) = &v {
assert!(arr.borrow().is_empty());
} else {
panic!("expected array");
}
}
#[test]
fn test_parse_flat_array() {
let v = json_parse("[1, 2, 3]", None).unwrap();
if let JsonValue::Array(arr) = &v {
let b = arr.borrow();
assert_eq!(b[0], JsonValue::Number(1.0));
assert_eq!(b[1], JsonValue::Number(2.0));
assert_eq!(b[2], JsonValue::Number(3.0));
} else {
panic!("expected array");
}
}
#[test]
fn test_parse_empty_object() {
let v = json_parse("{}", None).unwrap();
if let JsonValue::Object(obj) = &v {
assert!(obj.borrow().is_empty());
} else {
panic!("expected object");
}
}
#[test]
fn test_parse_flat_object() {
let v = json_parse(r#"{"a":1,"b":true}"#, None).unwrap();
if let JsonValue::Object(obj) = &v {
let b = obj.borrow();
assert_eq!(b[0], ("a".to_string(), JsonValue::Number(1.0)));
assert_eq!(b[1], ("b".to_string(), JsonValue::Bool(true)));
} else {
panic!("expected object");
}
}
#[test]
fn test_parse_nested_objects() {
let v = json_parse(r#"{"outer":{"inner":42}}"#, None).unwrap();
if let JsonValue::Object(obj) = &v {
let b = obj.borrow();
assert_eq!(b[0].0, "outer");
if let JsonValue::Object(inner) = &b[0].1 {
let ib = inner.borrow();
assert_eq!(ib[0], ("inner".to_string(), JsonValue::Number(42.0)));
} else {
panic!("expected inner object");
}
} else {
panic!("expected outer object");
}
}
#[test]
fn test_parse_nested_array_of_objects() {
let v = json_parse(r#"[{"x":1},{"x":2}]"#, None).unwrap();
if let JsonValue::Array(arr) = &v {
let b = arr.borrow();
assert_eq!(b.len(), 2);
for (i, item) in b.iter().enumerate() {
if let JsonValue::Object(obj) = item {
let ob = obj.borrow();
assert_eq!(ob[0].1, JsonValue::Number((i + 1) as f64));
} else {
panic!("expected object at index {i}");
}
}
} else {
panic!("expected array");
}
}
#[test]
fn test_parse_reviver_doubles_numbers() {
let v = json_parse(
"[1, 2, 3]",
Some(&|_key, val| {
Ok(Some(match val {
JsonValue::Number(n) => JsonValue::Number(n * 2.0),
other => other,
}))
}),
)
.unwrap();
if let JsonValue::Array(arr) = &v {
let b = arr.borrow();
assert_eq!(b[0], JsonValue::Number(2.0));
assert_eq!(b[1], JsonValue::Number(4.0));
assert_eq!(b[2], JsonValue::Number(6.0));
} else {
panic!("expected array");
}
}
#[test]
fn test_parse_reviver_deletes_object_property() {
let v = json_parse(
r#"{"a":1,"b":2,"c":3}"#,
Some(&|key, val| {
if key == "b" { Ok(None) } else { Ok(Some(val)) }
}),
)
.unwrap();
if let JsonValue::Object(obj) = &v {
let b = obj.borrow();
assert_eq!(b.len(), 2);
assert_eq!(b[0], ("a".to_string(), JsonValue::Number(1.0)));
assert_eq!(b[1], ("c".to_string(), JsonValue::Number(3.0)));
} else {
panic!("expected object");
}
}
#[test]
fn test_parse_reviver_array_undefined_becomes_null() {
let v = json_parse(
"[1, 2, 3]",
Some(&|key, val| {
if key == "1" { Ok(None) } else { Ok(Some(val)) }
}),
)
.unwrap();
if let JsonValue::Array(arr) = &v {
let b = arr.borrow();
assert_eq!(b[0], JsonValue::Number(1.0));
assert_eq!(b[1], JsonValue::Null);
assert_eq!(b[2], JsonValue::Number(3.0));
} else {
panic!("expected array");
}
}
#[test]
fn test_parse_trailing_garbage() {
assert!(json_parse("1 trailing", None).is_err());
}
#[test]
fn test_parse_invalid_number_no_digits_after_dot() {
assert!(json_parse("1.", None).is_err());
}
#[test]
fn test_parse_unterminated_string() {
assert!(json_parse(r#""no closing quote"#, None).is_err());
}
#[test]
fn test_parse_invalid_escape() {
assert!(json_parse(r#""\q""#, None).is_err());
}
#[test]
fn test_parse_empty_input() {
assert!(json_parse("", None).is_err());
}
#[test]
fn test_stringify_null() {
let s = json_stringify(&JsonValue::Null, None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "null");
}
#[test]
fn test_stringify_bool_true() {
let s = json_stringify(&JsonValue::Bool(true), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "true");
}
#[test]
fn test_stringify_bool_false() {
let s = json_stringify(&JsonValue::Bool(false), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "false");
}
#[test]
fn test_stringify_integer() {
let s = json_stringify(&JsonValue::Number(42.0), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "42");
}
#[test]
fn test_stringify_float() {
let s = json_stringify(&JsonValue::Number(3.14), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "3.14");
}
#[test]
fn test_stringify_nan_becomes_null() {
let s = json_stringify(&JsonValue::Number(f64::NAN), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "null");
}
#[test]
fn test_stringify_infinity_becomes_null() {
let s = json_stringify(&JsonValue::Number(f64::INFINITY), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "null");
}
#[test]
fn test_stringify_neg_infinity_becomes_null() {
let s = json_stringify(&JsonValue::Number(f64::NEG_INFINITY), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, "null");
}
#[test]
fn test_stringify_string() {
let s = json_stringify(&JsonValue::Str("hello".to_string()), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, r#""hello""#);
}
#[test]
fn test_stringify_string_with_special_chars() {
let s = json_stringify(
&JsonValue::Str("line1\nline2\ttab".to_string()),
None,
None,
None,
)
.unwrap()
.unwrap();
assert_eq!(s, r#""line1\nline2\ttab""#);
}
#[test]
fn test_stringify_string_with_quotes() {
let s = json_stringify(
&JsonValue::Str(r#"say "hello""#.to_string()),
None,
None,
None,
)
.unwrap()
.unwrap();
assert_eq!(s, r#""say \"hello\"""#);
}
#[test]
fn test_stringify_unicode_passthrough() {
let s = json_stringify(&JsonValue::Str("café 日本語".to_string()), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, r#""café 日本語""#);
}
#[test]
fn test_stringify_control_char_escaped() {
let s = json_stringify(&JsonValue::Str("\x01\x1F".to_string()), None, None, None)
.unwrap()
.unwrap();
assert_eq!(s, r#""\u0001\u001F""#);
}
#[test]
fn test_stringify_empty_array() {
let arr = JsonValue::new_array();
let s = json_stringify(&arr, None, None, None).unwrap().unwrap();
assert_eq!(s, "[]");
}
#[test]
fn test_stringify_flat_array() {
let arr = JsonValue::Array(Rc::new(RefCell::new(vec![
JsonValue::Number(1.0),
JsonValue::Number(2.0),
JsonValue::Number(3.0),
])));
let s = json_stringify(&arr, None, None, None).unwrap().unwrap();
assert_eq!(s, "[1,2,3]");
}
#[test]
fn test_stringify_empty_object() {
let obj = JsonValue::new_object();
let s = json_stringify(&obj, None, None, None).unwrap().unwrap();
assert_eq!(s, "{}");
}
#[test]
fn test_stringify_flat_object() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![
("a".to_string(), JsonValue::Number(1.0)),
("b".to_string(), JsonValue::Bool(true)),
])));
let s = json_stringify(&obj, None, None, None).unwrap().unwrap();
assert_eq!(s, r#"{"a":1,"b":true}"#);
}
#[test]
fn test_stringify_nested_objects() {
let inner = JsonValue::Object(Rc::new(RefCell::new(vec![(
"inner".to_string(),
JsonValue::Number(42.0),
)])));
let outer = JsonValue::Object(Rc::new(RefCell::new(vec![("outer".to_string(), inner)])));
let s = json_stringify(&outer, None, None, None).unwrap().unwrap();
assert_eq!(s, r#"{"outer":{"inner":42}}"#);
}
#[test]
fn test_stringify_with_count_indent() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![(
"x".to_string(),
JsonValue::Number(1.0),
)])));
let s = json_stringify(&obj, None, Some(&JsonSpace::Count(2)), None)
.unwrap()
.unwrap();
assert_eq!(s, "{\n \"x\": 1\n}");
}
#[test]
fn test_stringify_with_string_indent() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![(
"x".to_string(),
JsonValue::Number(1.0),
)])));
let s = json_stringify(&obj, None, Some(&JsonSpace::Str("\t".to_string())), None)
.unwrap()
.unwrap();
assert_eq!(s, "{\n\t\"x\": 1\n}");
}
#[test]
fn test_stringify_indent_clamped_to_10() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![(
"x".to_string(),
JsonValue::Number(1.0),
)])));
let s = json_stringify(&obj, None, Some(&JsonSpace::Count(20)), None)
.unwrap()
.unwrap();
let expected = "{\n \"x\": 1\n}";
assert_eq!(s, expected);
}
#[test]
fn test_stringify_replacer_array_filters_properties() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![
("a".to_string(), JsonValue::Number(1.0)),
("b".to_string(), JsonValue::Number(2.0)),
("c".to_string(), JsonValue::Number(3.0)),
])));
let replacer = JsonReplacer::Array(vec!["a".to_string(), "c".to_string()]);
let s = json_stringify(&obj, Some(&replacer), None, None)
.unwrap()
.unwrap();
assert_eq!(s, r#"{"a":1,"c":3}"#);
}
#[test]
fn test_stringify_replacer_function_transforms_values() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![
("a".to_string(), JsonValue::Number(1.0)),
("b".to_string(), JsonValue::Number(2.0)),
])));
let replacer = JsonReplacer::Function(&|_key, val| {
Ok(match val {
JsonValue::Number(n) => Some(JsonValue::Number(n * 10.0)),
other => Some(other.clone()),
})
});
let s = json_stringify(&obj, Some(&replacer), None, None)
.unwrap()
.unwrap();
assert_eq!(s, r#"{"a":10,"b":20}"#);
}
#[test]
fn test_stringify_replacer_function_omits_value() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![
("keep".to_string(), JsonValue::Number(1.0)),
("drop".to_string(), JsonValue::Number(2.0)),
])));
let replacer = JsonReplacer::Function(&|key, val| {
if key == "drop" {
Ok(None)
} else {
Ok(Some(val.clone()))
}
});
let s = json_stringify(&obj, Some(&replacer), None, None)
.unwrap()
.unwrap();
assert_eq!(s, r#"{"keep":1}"#);
}
#[test]
fn test_stringify_to_json_hook() {
let obj = JsonValue::Object(Rc::new(RefCell::new(vec![(
"val".to_string(),
JsonValue::Number(1.0),
)])));
let hook: &dyn Fn(&str, &JsonValue) -> Option<JsonValue> = &|_key, v| {
if v.is_object() {
Some(JsonValue::Str("custom".to_string()))
} else {
None
}
};
let s = json_stringify(&obj, None, None, Some(hook))
.unwrap()
.unwrap();
assert_eq!(s, r#""custom""#);
}
#[test]
fn test_stringify_circular_array_detected() {
let inner: Rc<RefCell<Vec<JsonValue>>> = Rc::new(RefCell::new(vec![]));
let outer = JsonValue::Array(inner.clone());
inner.borrow_mut().push(outer.clone());
let result = json_stringify(&outer, None, None, None);
assert!(
matches!(&result, Err(StatorError::TypeError(msg)) if msg.contains("circular")),
"expected circular structure TypeError, got {result:?}"
);
}
#[test]
fn test_stringify_circular_object_detected() {
let obj: Rc<RefCell<Vec<(String, JsonValue)>>> = Rc::new(RefCell::new(vec![]));
let self_ref = JsonValue::Object(obj.clone());
obj.borrow_mut()
.push(("self".to_string(), self_ref.clone()));
let result = json_stringify(&self_ref, None, None, None);
assert!(
matches!(&result, Err(StatorError::TypeError(msg)) if msg.contains("circular")),
"expected circular structure TypeError, got {result:?}"
);
}
#[test]
fn test_round_trip_primitives() {
for text in &["null", "true", "false", "42", "3.14", r#""hello""#] {
let parsed = json_parse(text, None).unwrap();
let stringified = json_stringify(&parsed, None, None, None).unwrap().unwrap();
assert_eq!(&stringified, text, "round-trip failed for {text}");
}
}
#[test]
fn test_round_trip_nested_object() {
let original = r#"{"a":1,"b":[2,3],"c":{"d":null}}"#;
let parsed = json_parse(original, None).unwrap();
let stringified = json_stringify(&parsed, None, None, None).unwrap().unwrap();
assert_eq!(stringified, original);
}
#[test]
fn test_round_trip_unicode_string() {
let original = r#""café 日本語 😀""#;
let parsed = json_parse(original, None).unwrap();
let stringified = json_stringify(&parsed, None, None, None).unwrap().unwrap();
assert_eq!(stringified, original);
}
#[test]
fn test_js_value_stringify_bigint_throws_type_error() {
let result = json_stringify_js_value(&JsValue::BigInt(Box::new(42)), None, None);
assert!(
matches!(&result, Err(StatorError::TypeError(msg)) if msg.contains("BigInt")),
"expected BigInt TypeError, got {result:?}"
);
}
#[test]
fn test_js_value_stringify_undefined_returns_none() {
let result = json_stringify_js_value(&JsValue::Undefined, None, None).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_js_value_stringify_null() {
let result = json_stringify_js_value(&JsValue::Null, None, None)
.unwrap()
.unwrap();
assert_eq!(result, "null");
}
#[test]
fn test_js_value_stringify_smi() {
let result = json_stringify_js_value(&JsValue::Smi(7), None, None)
.unwrap()
.unwrap();
assert_eq!(result, "7");
}
#[test]
fn test_js_value_stringify_boolean() {
let result = json_stringify_js_value(&JsValue::Boolean(true), None, None)
.unwrap()
.unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_js_value_stringify_heap_number_nan_null() {
let result = json_stringify_js_value(&JsValue::HeapNumber(f64::NAN), None, None)
.unwrap()
.unwrap();
assert_eq!(result, "null");
}
#[test]
fn test_js_value_stringify_array() {
let arr = JsValue::new_array(vec![
JsValue::Smi(1),
JsValue::Boolean(false),
JsValue::Null,
]);
let result = json_stringify_js_value(&arr, None, None).unwrap().unwrap();
assert_eq!(result, "[1,false,null]");
}
#[test]
fn test_js_value_to_json_with_to_json_method() {
use crate::objects::property_map::PropertyMap;
let mut inner = PropertyMap::new();
inner.insert("value".into(), JsValue::Smi(42));
inner.insert(
"toJSON".into(),
JsValue::NativeFunction(Rc::new(|_args| Ok(JsValue::String("replaced".into())))),
);
let obj = JsValue::PlainObject(Rc::new(RefCell::new(inner)));
let json = js_value_to_json(&obj).unwrap().unwrap();
assert_eq!(json, JsonValue::Str("replaced".to_string()));
}
#[test]
fn test_js_value_stringify_plain_object_with_replacer() {
use crate::objects::property_map::PropertyMap;
let mut map = PropertyMap::new();
map.insert("a".into(), JsValue::Smi(1));
map.insert("b".into(), JsValue::Smi(2));
map.insert("c".into(), JsValue::Smi(3));
let obj = JsValue::PlainObject(Rc::new(RefCell::new(map)));
let replacer = JsonReplacer::Array(vec!["a".to_string(), "c".to_string()]);
let s = json_stringify_js_value(&obj, Some(&replacer), None)
.unwrap()
.unwrap();
assert!(s.contains("\"a\""), "should contain a: {s}");
assert!(s.contains("\"c\""), "should contain c: {s}");
assert!(!s.contains("\"b\""), "should not contain b: {s}");
}
#[test]
fn test_js_value_stringify_with_space() {
let s = json_stringify_js_value(
&JsValue::new_array(vec![JsValue::Smi(1), JsValue::Smi(2)]),
None,
Some(&JsonSpace::Count(2)),
)
.unwrap()
.unwrap();
assert_eq!(s, "[\n 1,\n 2\n]");
}
#[test]
fn test_stringify_plain_object_integer_keys_first() {
use crate::objects::property_map::PropertyMap;
let mut map = PropertyMap::new();
map.insert("b".to_string(), JsValue::Smi(1));
map.insert("1".to_string(), JsValue::Smi(2));
map.insert("a".to_string(), JsValue::Smi(3));
map.insert("0".to_string(), JsValue::Smi(4));
let obj = JsValue::PlainObject(Rc::new(RefCell::new(map)));
let json = js_value_to_json(&obj).unwrap().unwrap();
let s = json_stringify(&json, None, None, None).unwrap().unwrap();
assert_eq!(s, r#"{"0":4,"1":2,"b":1,"a":3}"#);
}
#[test]
fn test_stringify_plain_object_sparse_indices() {
use crate::objects::property_map::PropertyMap;
let mut map = PropertyMap::new();
map.insert("2".to_string(), JsValue::String("c".into()));
map.insert("0".to_string(), JsValue::String("a".into()));
map.insert("1".to_string(), JsValue::String("b".into()));
let obj = JsValue::PlainObject(Rc::new(RefCell::new(map)));
let json = js_value_to_json(&obj).unwrap().unwrap();
let s = json_stringify(&json, None, None, None).unwrap().unwrap();
assert_eq!(s, r#"{"0":"a","1":"b","2":"c"}"#);
}
}