Skip to main content

parse_rust_core/
value.rs

1//! The `ParseValue` model: every JSON value a client can send or receive in an object body.
2//!
3//! Two decisions here are load-bearing, and both look like mistakes until the reason is stated.
4//!
5//! **`ParseMap` preserves key order.** Several upstream behaviors iterate object keys, and the
6//! resulting order is observable in golden-file comparison even where it is not semantically
7//! meaningful. A `HashMap` destroys it on every round trip and makes snapshot testing impossible.
8//!
9//! **Nothing here derives `PartialEq`.** A derived one would compile, read as correct, and get
10//! both float edge cases backwards at the one call site that decides which keys a client is told
11//! about. Use [`deep_strict_eq`], which implements Node's `util.isDeepStrictEqual` semantics.
12
13use indexmap::IndexMap;
14
15use crate::date::ParseDate;
16use crate::js_number;
17
18/// An order-preserving string-keyed map. See the module note.
19pub type ParseMap = IndexMap<String, ParseValue>;
20
21/// A Parse value.
22///
23/// The data plane stays dynamic on purpose. There is no compile-time struct per application
24/// class, because the schema does not exist at compile time. `Object` and `Array` contents are
25/// deliberately opaque.
26///
27/// Deliberately no `PartialEq`. See the module note.
28///
29/// **Deliberately not `#[non_exhaustive]`, either**, and that is the opposite of the usual
30/// advice. `non_exhaustive` lets a downstream crate keep compiling when a variant is added, by
31/// forcing it to carry a wildcard arm. For this type that is precisely the wrong trade: every
32/// consumer is a *total* function over the value space, an encoder, a decoder, a storage
33/// transform, an equality predicate, and a wildcard arm in any of them is a silent data-loss bug
34/// waiting for the next variant. The whole premise of writing this in Rust is that forgetting a
35/// case is a compile error; `non_exhaustive` on this enum would make that false across exactly
36/// the crate boundary that matters.
37///
38/// Adding a variant here is a breaking change on purpose. `ErrorCode` keeps `non_exhaustive`,
39/// because nobody exhaustively matches sixty error codes and a new one genuinely is additive.
40#[derive(Debug, Clone)]
41pub enum ParseValue {
42    Null,
43    Bool(bool),
44    /// JavaScript has exactly one number type. Matching its lossiness above 2^53 is the goal,
45    /// not avoiding it.
46    Number(f64),
47    String(String),
48    Array(Vec<ParseValue>),
49    Object(ParseMap),
50    /// `{"__type":"Date","iso":"..."}`, or a bare ISO string at `createdAt`/`updatedAt`.
51    Date(ParseDate),
52    /// `{"__type":"Pointer","className":"...","objectId":"..."}`
53    Pointer {
54        class_name: String,
55        object_id: String,
56    },
57    /// `{"__type":"GeoPoint","latitude":n,"longitude":n}`
58    GeoPoint {
59        latitude: f64,
60        longitude: f64,
61    },
62    /// `{"__type":"Bytes","base64":"..."}`. Held decoded, because Mongo stores BSON Binary and
63    /// re-encoding from a canonical byte slice is what keeps the two backends agreeing.
64    Bytes(Vec<u8>),
65    /// `{"__type":"File","name":"...","url":"..."}`. `url` is absent on a file pointer that has
66    /// not been through `expandFilesInObject`, so it is optional rather than defaulted.
67    File {
68        name: String,
69        url: Option<String>,
70    },
71    /// `{"__type":"Polygon","coordinates":[[lat,lng],...]}`
72    ///
73    /// Note the axis order: Parse's wire form is **latitude first**, the reverse of GeoJSON.
74    /// `PolygonCoder.databaseToJSON` swaps on the way out (`MongoTransform.js:1362-1372`), so
75    /// holding it in wire order keeps that swap confined to the Mongo boundary.
76    Polygon(Vec<(f64, f64)>),
77    /// `{"__type":"Relation","className":"..."}`
78    Relation {
79        class_name: String,
80    },
81}
82
83impl ParseValue {
84    /// Serialize to the exact bytes Parse Server would emit.
85    ///
86    /// Numbers go through [`js_number::to_ecma_string`] rather than any Rust float formatter,
87    /// for the reasons in that module. Non-finite numbers become `null`, which is what
88    /// `JSON.stringify` does; `to_ecma_string` alone would emit `NaN`, which is not valid JSON.
89    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                    // JSON.stringify(NaN) === "null", same for both infinities.
105                    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
193/// Standard base64 with padding, matching what `BytesCoder` accepts
194/// (`MongoTransform.js:1306`). Hand-rolled to keep `parse-rust-core` dependency-light; it is 20 lines
195/// and the alphabet is fixed by the wire format.
196pub(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
222/// The inverse. Rejects any character outside the alphabet rather than skipping it, because a
223/// lenient decoder would silently accept a corrupted payload from an untrusted client.
224pub(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
248/// JSON string escaping, matching `JSON.stringify`: the two mandatory escapes, the five
249/// short forms, and `\u00XX` for the rest of the C0 range. Characters above 0x1F are emitted
250/// as-is, including non-ASCII, which is what Node does.
251pub(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
271/// Deep equality with Node's `util.isDeepStrictEqual` semantics.
272///
273/// Two float cases differ from what a derived `PartialEq` would do, and both are reachable:
274/// Node compares primitives with `Object.is`, so **`NaN` equals `NaN`** and **`+0.0` does not
275/// equal `-0.0`**. `JSON.parse("-0")` yields `-0`, and a stored BSON double can be `-0`, so this
276/// is not theoretical.
277///
278/// Note the deliberate tension with the encoder: `-0.0` and `0.0` compare as distinct here and
279/// serialize identically (both as `0`). Both are correct, and both must hold at once.
280pub 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            // Key *order* is preserved by ParseMap but is not part of equality, matching Node,
292            // where two objects with the same keys in different orders are deep-strict-equal.
293            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
331/// `Object.is` for f64: NaN equals NaN, and zeros differ by sign.
332fn 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        // C0 controls take the \u00xx form, lowercase hex. Verified against Node:
409        //   JSON.stringify("a" + String.fromCharCode(1) + "b")  ->  "a\\u0001b"
410        assert_eq!(s("a\u{1}b").to_json(), "\"a\\u0001b\"");
411        assert_eq!(s("a\u{1f}b").to_json(), "\"a\\u001fb\"");
412        // Non-ASCII is emitted raw, as Node does.
413        assert_eq!(s("héllo").to_json(), "\"héllo\"");
414    }
415
416    #[test]
417    fn deep_strict_eq_follows_object_is_on_floats() {
418        // The two cases a derived PartialEq gets backwards.
419        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        // Both properties are required at once. This test exists to stop someone "fixing" one.
431        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        // No cross-type coercion: 1 is not "1" and not true.
462        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}