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.
196/// Base64, as the `{"__type":"Bytes","base64":...}` envelope spells it.
197///
198/// Public because the Mongo adapter needs the same spelling when it stores a value verbatim: schema
199/// metadata and query atoms both keep a `Bytes` in envelope form rather than as BSON Binary.
200pub 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
226/// The inverse. Rejects any character outside the alphabet rather than skipping it, because a
227/// lenient decoder would silently accept a corrupted payload from an untrusted client.
228/// Does this string match `BytesCoder.base64Pattern`?
229///
230/// ```text
231/// ^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$
232/// ```
233///
234/// Written out rather than pulled in as a regex because the shape is simple and the rule is exact:
235/// the total length is always a multiple of four, since the optional trailing group is four
236/// characters either way, and padding may only be the final one or two characters. The empty string
237/// matches, which upstream's pattern also allows.
238///
239/// **This decides whether a plain string stored in a `Bytes` column is a legacy Bytes value.**
240/// `isValidDatabaseObject` is `object instanceof mongodb.Binary || this.isBase64Value(object)`, so
241/// a column written by an older parse-server holds the string form and still has to raise to the
242/// envelope.
243pub 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
280/// JSON string escaping, matching `JSON.stringify`: the two mandatory escapes, the five
281/// short forms, and `\u00XX` for the rest of the C0 range. Characters above 0x1F are emitted
282/// as-is, including non-ASCII, which is what Node does.
283pub(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
303/// Deep equality with Node's `util.isDeepStrictEqual` semantics.
304///
305/// Two float cases differ from what a derived `PartialEq` would do, and both are reachable:
306/// Node compares primitives with `Object.is`, so **`NaN` equals `NaN`** and **`+0.0` does not
307/// equal `-0.0`**. `JSON.parse("-0")` yields `-0`, and a stored BSON double can be `-0`, so this
308/// is not theoretical.
309///
310/// Note the deliberate tension with the encoder: `-0.0` and `0.0` compare as distinct here and
311/// serialize identically (both as `0`). Both are correct, and both must hold at once.
312pub 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            // Key *order* is preserved by ParseMap but is not part of equality, matching Node,
324            // where two objects with the same keys in different orders are deep-strict-equal.
325            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
363/// `Object.is` for f64: NaN equals NaN, and zeros differ by sign.
364fn 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        // C0 controls take the \u00xx form, lowercase hex. Verified against Node:
441        //   JSON.stringify("a" + String.fromCharCode(1) + "b")  ->  "a\\u0001b"
442        assert_eq!(s("a\u{1}b").to_json(), "\"a\\u0001b\"");
443        assert_eq!(s("a\u{1f}b").to_json(), "\"a\\u001fb\"");
444        // Non-ASCII is emitted raw, as Node does.
445        assert_eq!(s("héllo").to_json(), "\"héllo\"");
446    }
447
448    #[test]
449    fn deep_strict_eq_follows_object_is_on_floats() {
450        // The two cases a derived PartialEq gets backwards.
451        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        // Both properties are required at once. This test exists to stop someone "fixing" one.
463        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        // No cross-type coercion: 1 is not "1" and not true.
494        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}