1use indexmap::IndexMap;
14
15use crate::date::ParseDate;
16use crate::js_number;
17
18pub type ParseMap = IndexMap<String, ParseValue>;
20
21#[derive(Debug, Clone)]
41pub enum ParseValue {
42 Null,
43 Bool(bool),
44 Number(f64),
47 String(String),
48 Array(Vec<ParseValue>),
49 Object(ParseMap),
50 Date(ParseDate),
52 Pointer {
54 class_name: String,
55 object_id: String,
56 },
57 GeoPoint {
59 latitude: f64,
60 longitude: f64,
61 },
62 Bytes(Vec<u8>),
65 File {
68 name: String,
69 url: Option<String>,
70 },
71 Polygon(Vec<(f64, f64)>),
77 Relation {
79 class_name: String,
80 },
81}
82
83impl ParseValue {
84 pub fn to_json(&self) -> String {
90 let mut s = String::new();
91 self.write_json(&mut s);
92 s
93 }
94
95 fn write_json(&self, out: &mut String) {
96 match self {
97 ParseValue::Null => out.push_str("null"),
98 ParseValue::Bool(true) => out.push_str("true"),
99 ParseValue::Bool(false) => out.push_str("false"),
100 ParseValue::Number(n) => {
101 if n.is_finite() {
102 out.push_str(&js_number::to_ecma_string(*n));
103 } else {
104 out.push_str("null");
106 }
107 }
108 ParseValue::String(s) => write_json_string(s, out),
109 ParseValue::Array(items) => {
110 out.push('[');
111 for (i, v) in items.iter().enumerate() {
112 if i > 0 {
113 out.push(',');
114 }
115 v.write_json(out);
116 }
117 out.push(']');
118 }
119 ParseValue::Object(map) => {
120 out.push('{');
121 for (i, (k, v)) in map.iter().enumerate() {
122 if i > 0 {
123 out.push(',');
124 }
125 write_json_string(k, out);
126 out.push(':');
127 v.write_json(out);
128 }
129 out.push('}');
130 }
131 ParseValue::Date(d) => {
132 out.push_str(r#"{"__type":"Date","iso":"#);
133 write_json_string(&d.to_iso(), out);
134 out.push('}');
135 }
136 ParseValue::Pointer {
137 class_name,
138 object_id,
139 } => {
140 out.push_str(r#"{"__type":"Pointer","className":"#);
141 write_json_string(class_name, out);
142 out.push_str(r#","objectId":"#);
143 write_json_string(object_id, out);
144 out.push('}');
145 }
146 ParseValue::GeoPoint {
147 latitude,
148 longitude,
149 } => {
150 out.push_str(r#"{"__type":"GeoPoint","latitude":"#);
151 out.push_str(&js_number::to_ecma_string(*latitude));
152 out.push_str(r#","longitude":"#);
153 out.push_str(&js_number::to_ecma_string(*longitude));
154 out.push('}');
155 }
156 ParseValue::Bytes(raw) => {
157 out.push_str(r#"{"__type":"Bytes","base64":"#);
158 write_json_string(&base64_encode(raw), out);
159 out.push('}');
160 }
161 ParseValue::File { name, url } => {
162 out.push_str(r#"{"__type":"File","name":"#);
163 write_json_string(name, out);
164 if let Some(u) = url {
165 out.push_str(r#","url":"#);
166 write_json_string(u, out);
167 }
168 out.push('}');
169 }
170 ParseValue::Polygon(coords) => {
171 out.push_str(r#"{"__type":"Polygon","coordinates":["#);
172 for (i, (lat, lng)) in coords.iter().enumerate() {
173 if i > 0 {
174 out.push(',');
175 }
176 out.push('[');
177 out.push_str(&js_number::to_ecma_string(*lat));
178 out.push(',');
179 out.push_str(&js_number::to_ecma_string(*lng));
180 out.push(']');
181 }
182 out.push_str("]}");
183 }
184 ParseValue::Relation { class_name } => {
185 out.push_str(r#"{"__type":"Relation","className":"#);
186 write_json_string(class_name, out);
187 out.push('}');
188 }
189 }
190 }
191}
192
193pub(crate) fn base64_encode(data: &[u8]) -> String {
197 const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
198 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
199 for chunk in data.chunks(3) {
200 let b = [
201 chunk[0],
202 *chunk.get(1).unwrap_or(&0),
203 *chunk.get(2).unwrap_or(&0),
204 ];
205 let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
206 out.push(T[(n >> 18) as usize & 63] as char);
207 out.push(T[(n >> 12) as usize & 63] as char);
208 out.push(if chunk.len() > 1 {
209 T[(n >> 6) as usize & 63] as char
210 } else {
211 '='
212 });
213 out.push(if chunk.len() > 2 {
214 T[n as usize & 63] as char
215 } else {
216 '='
217 });
218 }
219 out
220}
221
222pub(crate) fn base64_decode(s: &str) -> Option<Vec<u8>> {
225 let mut acc: u32 = 0;
226 let mut bits = 0u32;
227 let mut out = Vec::with_capacity(s.len() / 4 * 3);
228 for c in s.bytes() {
229 let v = match c {
230 b'A'..=b'Z' => c - b'A',
231 b'a'..=b'z' => c - b'a' + 26,
232 b'0'..=b'9' => c - b'0' + 52,
233 b'+' => 62,
234 b'/' => 63,
235 b'=' => break,
236 _ => return None,
237 } as u32;
238 acc = (acc << 6) | v;
239 bits += 6;
240 if bits >= 8 {
241 bits -= 8;
242 out.push((acc >> bits) as u8);
243 }
244 }
245 Some(out)
246}
247
248pub(crate) fn write_json_string(s: &str, out: &mut String) {
252 out.push('"');
253 for c in s.chars() {
254 match c {
255 '"' => out.push_str("\\\""),
256 '\\' => out.push_str("\\\\"),
257 '\n' => out.push_str("\\n"),
258 '\r' => out.push_str("\\r"),
259 '\t' => out.push_str("\\t"),
260 '\u{08}' => out.push_str("\\b"),
261 '\u{0c}' => out.push_str("\\f"),
262 c if (c as u32) < 0x20 => {
263 out.push_str(&format!("\\u{:04x}", c as u32));
264 }
265 c => out.push(c),
266 }
267 }
268 out.push('"');
269}
270
271pub fn deep_strict_eq(a: &ParseValue, b: &ParseValue) -> bool {
281 use ParseValue::*;
282 match (a, b) {
283 (Null, Null) => true,
284 (Bool(x), Bool(y)) => x == y,
285 (Number(x), Number(y)) => js_object_is(*x, *y),
286 (String(x), String(y)) => x == y,
287 (Array(x), Array(y)) => {
288 x.len() == y.len() && x.iter().zip(y).all(|(i, j)| deep_strict_eq(i, j))
289 }
290 (Object(x), Object(y)) => {
291 x.len() == y.len()
294 && x.iter()
295 .all(|(k, v)| y.get(k).is_some_and(|w| deep_strict_eq(v, w)))
296 }
297 (Date(x), Date(y)) => x == y,
298 (
299 Pointer {
300 class_name: c1,
301 object_id: o1,
302 },
303 Pointer {
304 class_name: c2,
305 object_id: o2,
306 },
307 ) => c1 == c2 && o1 == o2,
308 (
309 GeoPoint {
310 latitude: la1,
311 longitude: lo1,
312 },
313 GeoPoint {
314 latitude: la2,
315 longitude: lo2,
316 },
317 ) => js_object_is(*la1, *la2) && js_object_is(*lo1, *lo2),
318 (Bytes(x), Bytes(y)) => x == y,
319 (File { name: n1, url: u1 }, File { name: n2, url: u2 }) => n1 == n2 && u1 == u2,
320 (Polygon(x), Polygon(y)) => {
321 x.len() == y.len()
322 && x.iter()
323 .zip(y)
324 .all(|(a, b)| js_object_is(a.0, b.0) && js_object_is(a.1, b.1))
325 }
326 (Relation { class_name: c1 }, Relation { class_name: c2 }) => c1 == c2,
327 _ => false,
328 }
329}
330
331fn js_object_is(x: f64, y: f64) -> bool {
333 if x.is_nan() && y.is_nan() {
334 return true;
335 }
336 x == y && x.is_sign_negative() == y.is_sign_negative()
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 fn n(v: f64) -> ParseValue {
344 ParseValue::Number(v)
345 }
346 fn s(v: &str) -> ParseValue {
347 ParseValue::String(v.to_string())
348 }
349
350 #[test]
351 fn numbers_serialize_through_the_ecmascript_formatter() {
352 assert_eq!(n(100.0).to_json(), "100");
353 assert_eq!(n(1e20).to_json(), "100000000000000000000");
354 assert_eq!(n(1e-6).to_json(), "0.000001");
355 assert_eq!(n(-0.0).to_json(), "0");
356 }
357
358 #[test]
359 fn non_finite_numbers_become_null_not_nan() {
360 assert_eq!(n(f64::NAN).to_json(), "null");
361 assert_eq!(n(f64::INFINITY).to_json(), "null");
362 assert_eq!(n(f64::NEG_INFINITY).to_json(), "null");
363 }
364
365 #[test]
366 fn object_key_order_survives_serialization() {
367 let mut m = ParseMap::new();
368 m.insert("zebra".into(), n(1.0));
369 m.insert("apple".into(), n(2.0));
370 m.insert("mango".into(), n(3.0));
371 assert_eq!(
372 ParseValue::Object(m).to_json(),
373 r#"{"zebra":1,"apple":2,"mango":3}"#,
374 "insertion order must be preserved, not sorted"
375 );
376 }
377
378 #[test]
379 fn tagged_types_have_the_upstream_key_order() {
380 let d = ParseDate::parse_iso("2026-08-14T13:34:33.581Z").unwrap();
381 assert_eq!(
382 ParseValue::Date(d).to_json(),
383 r#"{"__type":"Date","iso":"2026-08-14T13:34:33.581Z"}"#
384 );
385 assert_eq!(
386 ParseValue::Pointer {
387 class_name: "_User".into(),
388 object_id: "abc123".into()
389 }
390 .to_json(),
391 r#"{"__type":"Pointer","className":"_User","objectId":"abc123"}"#
392 );
393 assert_eq!(
394 ParseValue::GeoPoint {
395 latitude: 40.0,
396 longitude: -75.5
397 }
398 .to_json(),
399 r#"{"__type":"GeoPoint","latitude":40,"longitude":-75.5}"#
400 );
401 }
402
403 #[test]
404 fn string_escaping_matches_json_stringify() {
405 assert_eq!(s(r#"a"b"#).to_json(), r#""a\"b""#);
406 assert_eq!(s("a\\b").to_json(), r#""a\\b""#);
407 assert_eq!(s("a\nb").to_json(), r#""a\nb""#);
408 assert_eq!(s("a\u{1}b").to_json(), "\"a\\u0001b\"");
411 assert_eq!(s("a\u{1f}b").to_json(), "\"a\\u001fb\"");
412 assert_eq!(s("héllo").to_json(), "\"héllo\"");
414 }
415
416 #[test]
417 fn deep_strict_eq_follows_object_is_on_floats() {
418 assert!(
420 deep_strict_eq(&n(f64::NAN), &n(f64::NAN)),
421 "NaN must equal NaN"
422 );
423 assert!(!deep_strict_eq(&n(0.0), &n(-0.0)), "+0 must not equal -0");
424 assert!(deep_strict_eq(&n(0.0), &n(0.0)));
425 assert!(deep_strict_eq(&n(-0.0), &n(-0.0)));
426 }
427
428 #[test]
429 fn minus_zero_compares_distinct_but_serializes_identically() {
430 assert!(!deep_strict_eq(&n(0.0), &n(-0.0)));
432 assert_eq!(n(0.0).to_json(), n(-0.0).to_json());
433 }
434
435 #[test]
436 fn deep_strict_eq_ignores_key_order_but_not_content() {
437 let mut a = ParseMap::new();
438 a.insert("x".into(), n(1.0));
439 a.insert("y".into(), n(2.0));
440 let mut b = ParseMap::new();
441 b.insert("y".into(), n(2.0));
442 b.insert("x".into(), n(1.0));
443 assert!(deep_strict_eq(
444 &ParseValue::Object(a.clone()),
445 &ParseValue::Object(b)
446 ));
447
448 let mut c = ParseMap::new();
449 c.insert("x".into(), n(1.0));
450 assert!(!deep_strict_eq(
451 &ParseValue::Object(a),
452 &ParseValue::Object(c)
453 ));
454 }
455
456 #[test]
457 fn deep_strict_eq_is_recursive_and_type_strict() {
458 let nested = |v: ParseValue| ParseValue::Array(vec![ParseValue::Array(vec![v])]);
459 assert!(deep_strict_eq(&nested(n(1.0)), &nested(n(1.0))));
460 assert!(!deep_strict_eq(&nested(n(1.0)), &nested(n(2.0))));
461 assert!(!deep_strict_eq(&n(1.0), &s("1")));
463 assert!(!deep_strict_eq(&n(1.0), &ParseValue::Bool(true)));
464 assert!(!deep_strict_eq(&ParseValue::Null, &ParseValue::Bool(false)));
465 }
466}