use indexmap::IndexMap;
use crate::date::ParseDate;
use crate::js_number;
pub type ParseMap = IndexMap<String, ParseValue>;
#[derive(Debug, Clone)]
pub enum ParseValue {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<ParseValue>),
Object(ParseMap),
Date(ParseDate),
Pointer {
class_name: String,
object_id: String,
},
GeoPoint {
latitude: f64,
longitude: f64,
},
Bytes(Vec<u8>),
File {
name: String,
url: Option<String>,
},
Polygon(Vec<(f64, f64)>),
Relation {
class_name: String,
},
}
impl ParseValue {
pub fn to_json(&self) -> String {
let mut s = String::new();
self.write_json(&mut s);
s
}
fn write_json(&self, out: &mut String) {
match self {
ParseValue::Null => out.push_str("null"),
ParseValue::Bool(true) => out.push_str("true"),
ParseValue::Bool(false) => out.push_str("false"),
ParseValue::Number(n) => {
if n.is_finite() {
out.push_str(&js_number::to_ecma_string(*n));
} else {
out.push_str("null");
}
}
ParseValue::String(s) => write_json_string(s, out),
ParseValue::Array(items) => {
out.push('[');
for (i, v) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
v.write_json(out);
}
out.push(']');
}
ParseValue::Object(map) => {
out.push('{');
for (i, (k, v)) in map.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_string(k, out);
out.push(':');
v.write_json(out);
}
out.push('}');
}
ParseValue::Date(d) => {
out.push_str(r#"{"__type":"Date","iso":"#);
write_json_string(&d.to_iso(), out);
out.push('}');
}
ParseValue::Pointer {
class_name,
object_id,
} => {
out.push_str(r#"{"__type":"Pointer","className":"#);
write_json_string(class_name, out);
out.push_str(r#","objectId":"#);
write_json_string(object_id, out);
out.push('}');
}
ParseValue::GeoPoint {
latitude,
longitude,
} => {
out.push_str(r#"{"__type":"GeoPoint","latitude":"#);
out.push_str(&js_number::to_ecma_string(*latitude));
out.push_str(r#","longitude":"#);
out.push_str(&js_number::to_ecma_string(*longitude));
out.push('}');
}
ParseValue::Bytes(raw) => {
out.push_str(r#"{"__type":"Bytes","base64":"#);
write_json_string(&base64_encode(raw), out);
out.push('}');
}
ParseValue::File { name, url } => {
out.push_str(r#"{"__type":"File","name":"#);
write_json_string(name, out);
if let Some(u) = url {
out.push_str(r#","url":"#);
write_json_string(u, out);
}
out.push('}');
}
ParseValue::Polygon(coords) => {
out.push_str(r#"{"__type":"Polygon","coordinates":["#);
for (i, (lat, lng)) in coords.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('[');
out.push_str(&js_number::to_ecma_string(*lat));
out.push(',');
out.push_str(&js_number::to_ecma_string(*lng));
out.push(']');
}
out.push_str("]}");
}
ParseValue::Relation { class_name } => {
out.push_str(r#"{"__type":"Relation","className":"#);
write_json_string(class_name, out);
out.push('}');
}
}
}
}
pub(crate) fn base64_encode(data: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b = [
chunk[0],
*chunk.get(1).unwrap_or(&0),
*chunk.get(2).unwrap_or(&0),
];
let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
out.push(T[(n >> 18) as usize & 63] as char);
out.push(T[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
T[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
T[n as usize & 63] as char
} else {
'='
});
}
out
}
pub(crate) fn base64_decode(s: &str) -> Option<Vec<u8>> {
let mut acc: u32 = 0;
let mut bits = 0u32;
let mut out = Vec::with_capacity(s.len() / 4 * 3);
for c in s.bytes() {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
b'=' => break,
_ => return None,
} as u32;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
}
}
Some(out)
}
pub(crate) fn write_json_string(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
pub fn deep_strict_eq(a: &ParseValue, b: &ParseValue) -> bool {
use ParseValue::*;
match (a, b) {
(Null, Null) => true,
(Bool(x), Bool(y)) => x == y,
(Number(x), Number(y)) => js_object_is(*x, *y),
(String(x), String(y)) => x == y,
(Array(x), Array(y)) => {
x.len() == y.len() && x.iter().zip(y).all(|(i, j)| deep_strict_eq(i, j))
}
(Object(x), Object(y)) => {
x.len() == y.len()
&& x.iter()
.all(|(k, v)| y.get(k).is_some_and(|w| deep_strict_eq(v, w)))
}
(Date(x), Date(y)) => x == y,
(
Pointer {
class_name: c1,
object_id: o1,
},
Pointer {
class_name: c2,
object_id: o2,
},
) => c1 == c2 && o1 == o2,
(
GeoPoint {
latitude: la1,
longitude: lo1,
},
GeoPoint {
latitude: la2,
longitude: lo2,
},
) => js_object_is(*la1, *la2) && js_object_is(*lo1, *lo2),
(Bytes(x), Bytes(y)) => x == y,
(File { name: n1, url: u1 }, File { name: n2, url: u2 }) => n1 == n2 && u1 == u2,
(Polygon(x), Polygon(y)) => {
x.len() == y.len()
&& x.iter()
.zip(y)
.all(|(a, b)| js_object_is(a.0, b.0) && js_object_is(a.1, b.1))
}
(Relation { class_name: c1 }, Relation { class_name: c2 }) => c1 == c2,
_ => false,
}
}
fn js_object_is(x: f64, y: f64) -> bool {
if x.is_nan() && y.is_nan() {
return true;
}
x == y && x.is_sign_negative() == y.is_sign_negative()
}
#[cfg(test)]
mod tests {
use super::*;
fn n(v: f64) -> ParseValue {
ParseValue::Number(v)
}
fn s(v: &str) -> ParseValue {
ParseValue::String(v.to_string())
}
#[test]
fn numbers_serialize_through_the_ecmascript_formatter() {
assert_eq!(n(100.0).to_json(), "100");
assert_eq!(n(1e20).to_json(), "100000000000000000000");
assert_eq!(n(1e-6).to_json(), "0.000001");
assert_eq!(n(-0.0).to_json(), "0");
}
#[test]
fn non_finite_numbers_become_null_not_nan() {
assert_eq!(n(f64::NAN).to_json(), "null");
assert_eq!(n(f64::INFINITY).to_json(), "null");
assert_eq!(n(f64::NEG_INFINITY).to_json(), "null");
}
#[test]
fn object_key_order_survives_serialization() {
let mut m = ParseMap::new();
m.insert("zebra".into(), n(1.0));
m.insert("apple".into(), n(2.0));
m.insert("mango".into(), n(3.0));
assert_eq!(
ParseValue::Object(m).to_json(),
r#"{"zebra":1,"apple":2,"mango":3}"#,
"insertion order must be preserved, not sorted"
);
}
#[test]
fn tagged_types_have_the_upstream_key_order() {
let d = ParseDate::parse_iso("2026-08-14T13:34:33.581Z").unwrap();
assert_eq!(
ParseValue::Date(d).to_json(),
r#"{"__type":"Date","iso":"2026-08-14T13:34:33.581Z"}"#
);
assert_eq!(
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "abc123".into()
}
.to_json(),
r#"{"__type":"Pointer","className":"_User","objectId":"abc123"}"#
);
assert_eq!(
ParseValue::GeoPoint {
latitude: 40.0,
longitude: -75.5
}
.to_json(),
r#"{"__type":"GeoPoint","latitude":40,"longitude":-75.5}"#
);
}
#[test]
fn string_escaping_matches_json_stringify() {
assert_eq!(s(r#"a"b"#).to_json(), r#""a\"b""#);
assert_eq!(s("a\\b").to_json(), r#""a\\b""#);
assert_eq!(s("a\nb").to_json(), r#""a\nb""#);
assert_eq!(s("a\u{1}b").to_json(), "\"a\\u0001b\"");
assert_eq!(s("a\u{1f}b").to_json(), "\"a\\u001fb\"");
assert_eq!(s("héllo").to_json(), "\"héllo\"");
}
#[test]
fn deep_strict_eq_follows_object_is_on_floats() {
assert!(
deep_strict_eq(&n(f64::NAN), &n(f64::NAN)),
"NaN must equal NaN"
);
assert!(!deep_strict_eq(&n(0.0), &n(-0.0)), "+0 must not equal -0");
assert!(deep_strict_eq(&n(0.0), &n(0.0)));
assert!(deep_strict_eq(&n(-0.0), &n(-0.0)));
}
#[test]
fn minus_zero_compares_distinct_but_serializes_identically() {
assert!(!deep_strict_eq(&n(0.0), &n(-0.0)));
assert_eq!(n(0.0).to_json(), n(-0.0).to_json());
}
#[test]
fn deep_strict_eq_ignores_key_order_but_not_content() {
let mut a = ParseMap::new();
a.insert("x".into(), n(1.0));
a.insert("y".into(), n(2.0));
let mut b = ParseMap::new();
b.insert("y".into(), n(2.0));
b.insert("x".into(), n(1.0));
assert!(deep_strict_eq(
&ParseValue::Object(a.clone()),
&ParseValue::Object(b)
));
let mut c = ParseMap::new();
c.insert("x".into(), n(1.0));
assert!(!deep_strict_eq(
&ParseValue::Object(a),
&ParseValue::Object(c)
));
}
#[test]
fn deep_strict_eq_is_recursive_and_type_strict() {
let nested = |v: ParseValue| ParseValue::Array(vec![ParseValue::Array(vec![v])]);
assert!(deep_strict_eq(&nested(n(1.0)), &nested(n(1.0))));
assert!(!deep_strict_eq(&nested(n(1.0)), &nested(n(2.0))));
assert!(!deep_strict_eq(&n(1.0), &s("1")));
assert!(!deep_strict_eq(&n(1.0), &ParseValue::Bool(true)));
assert!(!deep_strict_eq(&ParseValue::Null, &ParseValue::Bool(false)));
}
}