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 fn base64_encode(data: &[u8]) -> String {
201 const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
202 let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
203 for chunk in data.chunks(3) {
204 let b = [
205 chunk[0],
206 *chunk.get(1).unwrap_or(&0),
207 *chunk.get(2).unwrap_or(&0),
208 ];
209 let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
210 out.push(T[(n >> 18) as usize & 63] as char);
211 out.push(T[(n >> 12) as usize & 63] as char);
212 out.push(if chunk.len() > 1 {
213 T[(n >> 6) as usize & 63] as char
214 } else {
215 '='
216 });
217 out.push(if chunk.len() > 2 {
218 T[n as usize & 63] as char
219 } else {
220 '='
221 });
222 }
223 out
224}
225
226pub fn is_base64_value(s: &str) -> bool {
244 if !s.len().is_multiple_of(4) {
245 return false;
246 }
247 let padding = s.bytes().rev().take_while(|b| *b == b'=').count();
248 if padding > 2 {
249 return false;
250 }
251 s.as_bytes()[..s.len() - padding]
252 .iter()
253 .all(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/')
254}
255
256pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
257 let mut acc: u32 = 0;
258 let mut bits = 0u32;
259 let mut out = Vec::with_capacity(s.len() / 4 * 3);
260 for c in s.bytes() {
261 let v = match c {
262 b'A'..=b'Z' => c - b'A',
263 b'a'..=b'z' => c - b'a' + 26,
264 b'0'..=b'9' => c - b'0' + 52,
265 b'+' => 62,
266 b'/' => 63,
267 b'=' => break,
268 _ => return None,
269 } as u32;
270 acc = (acc << 6) | v;
271 bits += 6;
272 if bits >= 8 {
273 bits -= 8;
274 out.push((acc >> bits) as u8);
275 }
276 }
277 Some(out)
278}
279
280pub(crate) fn write_json_string(s: &str, out: &mut String) {
284 out.push('"');
285 for c in s.chars() {
286 match c {
287 '"' => out.push_str("\\\""),
288 '\\' => out.push_str("\\\\"),
289 '\n' => out.push_str("\\n"),
290 '\r' => out.push_str("\\r"),
291 '\t' => out.push_str("\\t"),
292 '\u{08}' => out.push_str("\\b"),
293 '\u{0c}' => out.push_str("\\f"),
294 c if (c as u32) < 0x20 => {
295 out.push_str(&format!("\\u{:04x}", c as u32));
296 }
297 c => out.push(c),
298 }
299 }
300 out.push('"');
301}
302
303pub fn deep_strict_eq(a: &ParseValue, b: &ParseValue) -> bool {
313 use ParseValue::*;
314 match (a, b) {
315 (Null, Null) => true,
316 (Bool(x), Bool(y)) => x == y,
317 (Number(x), Number(y)) => js_object_is(*x, *y),
318 (String(x), String(y)) => x == y,
319 (Array(x), Array(y)) => {
320 x.len() == y.len() && x.iter().zip(y).all(|(i, j)| deep_strict_eq(i, j))
321 }
322 (Object(x), Object(y)) => {
323 x.len() == y.len()
326 && x.iter()
327 .all(|(k, v)| y.get(k).is_some_and(|w| deep_strict_eq(v, w)))
328 }
329 (Date(x), Date(y)) => x == y,
330 (
331 Pointer {
332 class_name: c1,
333 object_id: o1,
334 },
335 Pointer {
336 class_name: c2,
337 object_id: o2,
338 },
339 ) => c1 == c2 && o1 == o2,
340 (
341 GeoPoint {
342 latitude: la1,
343 longitude: lo1,
344 },
345 GeoPoint {
346 latitude: la2,
347 longitude: lo2,
348 },
349 ) => js_object_is(*la1, *la2) && js_object_is(*lo1, *lo2),
350 (Bytes(x), Bytes(y)) => x == y,
351 (File { name: n1, url: u1 }, File { name: n2, url: u2 }) => n1 == n2 && u1 == u2,
352 (Polygon(x), Polygon(y)) => {
353 x.len() == y.len()
354 && x.iter()
355 .zip(y)
356 .all(|(a, b)| js_object_is(a.0, b.0) && js_object_is(a.1, b.1))
357 }
358 (Relation { class_name: c1 }, Relation { class_name: c2 }) => c1 == c2,
359 _ => false,
360 }
361}
362
363fn js_object_is(x: f64, y: f64) -> bool {
365 if x.is_nan() && y.is_nan() {
366 return true;
367 }
368 x == y && x.is_sign_negative() == y.is_sign_negative()
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 fn n(v: f64) -> ParseValue {
376 ParseValue::Number(v)
377 }
378 fn s(v: &str) -> ParseValue {
379 ParseValue::String(v.to_string())
380 }
381
382 #[test]
383 fn numbers_serialize_through_the_ecmascript_formatter() {
384 assert_eq!(n(100.0).to_json(), "100");
385 assert_eq!(n(1e20).to_json(), "100000000000000000000");
386 assert_eq!(n(1e-6).to_json(), "0.000001");
387 assert_eq!(n(-0.0).to_json(), "0");
388 }
389
390 #[test]
391 fn non_finite_numbers_become_null_not_nan() {
392 assert_eq!(n(f64::NAN).to_json(), "null");
393 assert_eq!(n(f64::INFINITY).to_json(), "null");
394 assert_eq!(n(f64::NEG_INFINITY).to_json(), "null");
395 }
396
397 #[test]
398 fn object_key_order_survives_serialization() {
399 let mut m = ParseMap::new();
400 m.insert("zebra".into(), n(1.0));
401 m.insert("apple".into(), n(2.0));
402 m.insert("mango".into(), n(3.0));
403 assert_eq!(
404 ParseValue::Object(m).to_json(),
405 r#"{"zebra":1,"apple":2,"mango":3}"#,
406 "insertion order must be preserved, not sorted"
407 );
408 }
409
410 #[test]
411 fn tagged_types_have_the_upstream_key_order() {
412 let d = ParseDate::parse_iso("2026-08-14T13:34:33.581Z").unwrap();
413 assert_eq!(
414 ParseValue::Date(d).to_json(),
415 r#"{"__type":"Date","iso":"2026-08-14T13:34:33.581Z"}"#
416 );
417 assert_eq!(
418 ParseValue::Pointer {
419 class_name: "_User".into(),
420 object_id: "abc123".into()
421 }
422 .to_json(),
423 r#"{"__type":"Pointer","className":"_User","objectId":"abc123"}"#
424 );
425 assert_eq!(
426 ParseValue::GeoPoint {
427 latitude: 40.0,
428 longitude: -75.5
429 }
430 .to_json(),
431 r#"{"__type":"GeoPoint","latitude":40,"longitude":-75.5}"#
432 );
433 }
434
435 #[test]
436 fn string_escaping_matches_json_stringify() {
437 assert_eq!(s(r#"a"b"#).to_json(), r#""a\"b""#);
438 assert_eq!(s("a\\b").to_json(), r#""a\\b""#);
439 assert_eq!(s("a\nb").to_json(), r#""a\nb""#);
440 assert_eq!(s("a\u{1}b").to_json(), "\"a\\u0001b\"");
443 assert_eq!(s("a\u{1f}b").to_json(), "\"a\\u001fb\"");
444 assert_eq!(s("héllo").to_json(), "\"héllo\"");
446 }
447
448 #[test]
449 fn deep_strict_eq_follows_object_is_on_floats() {
450 assert!(
452 deep_strict_eq(&n(f64::NAN), &n(f64::NAN)),
453 "NaN must equal NaN"
454 );
455 assert!(!deep_strict_eq(&n(0.0), &n(-0.0)), "+0 must not equal -0");
456 assert!(deep_strict_eq(&n(0.0), &n(0.0)));
457 assert!(deep_strict_eq(&n(-0.0), &n(-0.0)));
458 }
459
460 #[test]
461 fn minus_zero_compares_distinct_but_serializes_identically() {
462 assert!(!deep_strict_eq(&n(0.0), &n(-0.0)));
464 assert_eq!(n(0.0).to_json(), n(-0.0).to_json());
465 }
466
467 #[test]
468 fn deep_strict_eq_ignores_key_order_but_not_content() {
469 let mut a = ParseMap::new();
470 a.insert("x".into(), n(1.0));
471 a.insert("y".into(), n(2.0));
472 let mut b = ParseMap::new();
473 b.insert("y".into(), n(2.0));
474 b.insert("x".into(), n(1.0));
475 assert!(deep_strict_eq(
476 &ParseValue::Object(a.clone()),
477 &ParseValue::Object(b)
478 ));
479
480 let mut c = ParseMap::new();
481 c.insert("x".into(), n(1.0));
482 assert!(!deep_strict_eq(
483 &ParseValue::Object(a),
484 &ParseValue::Object(c)
485 ));
486 }
487
488 #[test]
489 fn deep_strict_eq_is_recursive_and_type_strict() {
490 let nested = |v: ParseValue| ParseValue::Array(vec![ParseValue::Array(vec![v])]);
491 assert!(deep_strict_eq(&nested(n(1.0)), &nested(n(1.0))));
492 assert!(!deep_strict_eq(&nested(n(1.0)), &nested(n(2.0))));
493 assert!(!deep_strict_eq(&n(1.0), &s("1")));
495 assert!(!deep_strict_eq(&n(1.0), &ParseValue::Bool(true)));
496 assert!(!deep_strict_eq(&ParseValue::Null, &ParseValue::Bool(false)));
497 }
498}