Skip to main content

nodedb_types/json_msgpack/
writer.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Msgpack serialization for `serde_json::Value` and `nodedb_types::Value`.
4//!
5//! `Value::DateTime` and `Value::NaiveDateTime` are written as the instant
6//! ext (`fixext8`, see `instant_ext`). `Duration`, `Decimal`, and `Geometry`
7//! are written as strings, except a `Decimal` that holds a `u64` above
8//! `i64::MAX`, which is a `uint64`. `Vector` is a float64 array, `ArrayCell`
9//! a map, and `Range` / `Record` are `nil`.
10
11use zerompk::Write;
12
13use super::instant_ext::InstantKind;
14use super::json_value::JsonValue;
15
16/// Serialize a `serde_json::Value` to MessagePack bytes.
17#[inline]
18pub fn json_to_msgpack(value: &serde_json::Value) -> zerompk::Result<Vec<u8>> {
19    zerompk::to_msgpack_vec(&JsonValue(value.clone()))
20}
21
22/// Serialize a `serde_json::Value` to MessagePack bytes, returning an empty
23/// msgpack map (`0x80`) on failure.
24///
25/// Suitable for filter evaluation where an empty map causes all field
26/// predicates to pass vacuously. Callers that need error propagation
27/// should use [`json_to_msgpack`] instead.
28#[inline]
29pub fn json_to_msgpack_or_empty(value: &serde_json::Value) -> Vec<u8> {
30    json_to_msgpack(value).unwrap_or_else(|_| vec![0x80])
31}
32
33/// Serialize a `nodedb_types::Value` to standard MessagePack bytes.
34///
35/// Writes standard msgpack format (fixmap 0x80-0x8F, fixstr 0xA0-0xBF, etc.)
36/// directly from `Value` — no zerompk tagged encoding.
37pub fn value_to_msgpack(value: &crate::Value) -> zerompk::Result<Vec<u8>> {
38    zerompk::to_msgpack_vec(&NativeRef(value))
39}
40
41/// A borrowed `Value` written in its plain msgpack form.
42struct NativeRef<'a>(&'a crate::Value);
43
44impl zerompk::ToMessagePack for NativeRef<'_> {
45    fn write<W: Write>(&self, writer: &mut W) -> zerompk::Result<()> {
46        write_native_value(writer, self.0)
47    }
48}
49
50/// Write a `nodedb_types::Value` as standard msgpack.
51///
52/// `Duration`, `Decimal`, and `Geometry` are strings, except a `Decimal`
53/// that holds a `u64` above `i64::MAX` (see [`crate::Value::decimal_as_wide_u64`]),
54/// which is a `uint64`. `Vector` is a float64 array, `ArrayCell` a map, and
55/// `Range` / `Record` are `nil`.
56pub(crate) fn write_native_value<W: Write>(
57    writer: &mut W,
58    value: &crate::Value,
59) -> zerompk::Result<()> {
60    match value {
61        crate::Value::Null => writer.write_nil(),
62        crate::Value::Bool(b) => writer.write_boolean(*b),
63        crate::Value::Integer(i) => writer.write_i64(*i),
64        crate::Value::Float(f) => writer.write_f64(*f),
65        crate::Value::String(s)
66        | crate::Value::Uuid(s)
67        | crate::Value::Ulid(s)
68        | crate::Value::Regex(s) => writer.write_string(s),
69        crate::Value::Bytes(b) => writer.write_binary(b),
70        crate::Value::Array(arr) | crate::Value::Set(arr) => {
71            writer.write_array_len(arr.len())?;
72            for v in arr {
73                write_native_value(writer, v)?;
74            }
75            Ok(())
76        }
77        crate::Value::Object(map) => {
78            writer.write_map_len(map.len())?;
79            for (k, v) in map {
80                writer.write_string(k)?;
81                write_native_value(writer, v)?;
82            }
83            Ok(())
84        }
85        crate::Value::DateTime(dt) => write_instant_ext(writer, InstantKind::Utc, dt.micros),
86        crate::Value::NaiveDateTime(dt) => write_instant_ext(writer, InstantKind::Naive, dt.micros),
87        crate::Value::Duration(d) => writer.write_string(&d.to_string()),
88        crate::Value::Decimal(d) => match crate::Value::decimal_as_wide_u64(d) {
89            Some(u) => writer.write_u64(u),
90            None => writer.write_string(&d.to_string()),
91        },
92        crate::Value::Geometry(g) => match sonic_rs::to_string(g) {
93            Ok(s) => writer.write_string(&s),
94            Err(_) => writer.write_nil(),
95        },
96        crate::Value::Range { .. } | crate::Value::Record { .. } => writer.write_nil(),
97        crate::Value::Vector(v) => {
98            // A standard msgpack array of float64 values, so pgwire clients
99            // receive a plain JSON number array.
100            writer.write_array_len(v.len())?;
101            for f in v.iter() {
102                writer.write_f64(f64::from(*f))?;
103            }
104            Ok(())
105        }
106        // ArrayCell is encoded as a `{coords:[...], attrs:[...]}` map so the
107        // pgwire `msgpack_to_json_string` transcoder produces clean JSON for
108        // clients reading slice/project rows. On audit-log (AS OF SYSTEM TIME
109        // NULL) reads each version carries its system-time, surfaced as the
110        // `_ts_system` column (mirrors the document-engine audit-log shape).
111        crate::Value::ArrayCell(cell) => {
112            let map_len = if cell.system_time.is_some() { 3 } else { 2 };
113            writer.write_map_len(map_len)?;
114            writer.write_string("coords")?;
115            writer.write_array_len(cell.coords.len())?;
116            for v in &cell.coords {
117                write_native_value(writer, v)?;
118            }
119            writer.write_string("attrs")?;
120            writer.write_array_len(cell.attrs.len())?;
121            for v in &cell.attrs {
122                write_native_value(writer, v)?;
123            }
124            if let Some(ts) = cell.system_time {
125                writer.write_string("_ts_system")?;
126                writer.write_i64(ts)?;
127            }
128            Ok(())
129        }
130    }
131}
132
133/// Write an instant as the `fixext8` ext `write_instant` produces.
134fn write_instant_ext<W: Write>(
135    writer: &mut W,
136    kind: InstantKind,
137    micros: i64,
138) -> zerompk::Result<()> {
139    writer.write_ext(kind.ext_type(), &micros.to_be_bytes())
140}