Skip to main content

reddb_types/
value_json.rs

1use crate::json::{from_slice as json_from_slice, Map, Value as JsonValue};
2use crate::Value;
3
4const MAX_JSON_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
5
6impl Value {
7    /// Render this value using RedDB's canonical JSON representation.
8    ///
9    /// Integers outside JavaScript's exactly representable range and fixed-point
10    /// decimals use tagged objects so no precision is lost at a JSON boundary.
11    pub fn to_json(&self) -> JsonValue {
12        match self {
13            Value::Null => JsonValue::Null,
14            Value::Integer(value) => exact_i64_to_json(*value),
15            Value::UnsignedInteger(value) => exact_u64_to_json(*value),
16            Value::Float(value) => JsonValue::Number(*value),
17            Value::Text(value) => JsonValue::String(value.to_string()),
18            Value::Blob(value) => JsonValue::String(hex_encode(value)),
19            Value::Boolean(value) => JsonValue::Bool(*value),
20            Value::Timestamp(value) => exact_i64_to_json(*value),
21            Value::Duration(value) => exact_i64_to_json(*value),
22            Value::IpAddr(value) => JsonValue::String(value.to_string()),
23            Value::MacAddr(value) => JsonValue::String(format_mac(value)),
24            Value::Vector(value) => JsonValue::Array(
25                value
26                    .iter()
27                    .map(|entry| JsonValue::Number(f64::from(*entry)))
28                    .collect(),
29            ),
30            Value::Json(value) => json_bytes_to_json(value),
31            Value::Uuid(value) => JsonValue::String(hex_encode(value)),
32            Value::NodeRef(value) | Value::EdgeRef(value) => JsonValue::String(value.clone()),
33            Value::VectorRef(collection, id) => JsonValue::Object(
34                [
35                    (
36                        "collection".to_string(),
37                        JsonValue::String(collection.clone()),
38                    ),
39                    ("id".to_string(), exact_u64_to_json(*id)),
40                ]
41                .into_iter()
42                .collect(),
43            ),
44            Value::RowRef(table, row_id) => JsonValue::Object(
45                [
46                    ("table".to_string(), JsonValue::String(table.clone())),
47                    ("row_id".to_string(), exact_u64_to_json(*row_id)),
48                ]
49                .into_iter()
50                .collect(),
51            ),
52            Value::Color([r, g, b]) => JsonValue::String(format!("#{r:02X}{g:02X}{b:02X}")),
53            Value::Email(value) | Value::Url(value) => JsonValue::String(value.clone()),
54            Value::Phone(value) => exact_u64_to_json(*value),
55            Value::Semver(packed) => JsonValue::String(format!(
56                "{}.{}.{}",
57                packed / 1_000_000,
58                (packed / 1_000) % 1_000,
59                packed % 1_000
60            )),
61            Value::Cidr(ip, prefix) => JsonValue::String(format!(
62                "{}.{}.{}.{}/{}",
63                (ip >> 24) & 0xFF,
64                (ip >> 16) & 0xFF,
65                (ip >> 8) & 0xFF,
66                ip & 0xFF,
67                prefix
68            )),
69            Value::Date(days) => JsonValue::Integer(i64::from(*days)),
70            Value::Time(milliseconds) => JsonValue::Integer(i64::from(*milliseconds)),
71            Value::Decimal(_) => exact_decimal_to_json(self.display_string()),
72            Value::DecimalText(value) => exact_decimal_to_json(value.clone()),
73            Value::EnumValue(value) => JsonValue::Integer(i64::from(*value)),
74            Value::Array(values) => JsonValue::Array(values.iter().map(Value::to_json).collect()),
75            Value::TimestampMs(value) => exact_i64_to_json(*value),
76            Value::Ipv4(ip) => JsonValue::String(format_ipv4(*ip)),
77            Value::Ipv6(bytes) => JsonValue::String(std::net::Ipv6Addr::from(*bytes).to_string()),
78            Value::Subnet(ip, mask) => {
79                JsonValue::String(format!("{}/{}", format_ipv4(*ip), mask.leading_ones()))
80            }
81            Value::Port(value) => JsonValue::Integer(i64::from(*value)),
82            Value::Latitude(microdegrees) | Value::Longitude(microdegrees) => {
83                JsonValue::Number(f64::from(*microdegrees) / 1_000_000.0)
84            }
85            Value::GeoPoint(latitude, longitude) => JsonValue::String(format!(
86                "{:.6},{:.6}",
87                f64::from(*latitude) / 1_000_000.0,
88                f64::from(*longitude) / 1_000_000.0
89            )),
90            Value::Country2(code) | Value::Lang2(code) => {
91                JsonValue::String(String::from_utf8_lossy(code).to_string())
92            }
93            Value::Country3(code) | Value::Currency(code) => {
94                JsonValue::String(String::from_utf8_lossy(code).to_string())
95            }
96            Value::Lang5(code) => JsonValue::String(String::from_utf8_lossy(code).to_string()),
97            Value::AssetCode(code) => JsonValue::String(code.clone()),
98            Value::Money {
99                asset_code,
100                minor_units,
101                scale,
102            } => JsonValue::Object(
103                [
104                    (
105                        "asset_code".to_string(),
106                        JsonValue::String(asset_code.clone()),
107                    ),
108                    ("minor_units".to_string(), exact_i64_to_json(*minor_units)),
109                    ("scale".to_string(), exact_i64_to_json(i64::from(*scale))),
110                ]
111                .into_iter()
112                .collect(),
113            ),
114            Value::ColorAlpha([r, g, b, a]) => {
115                JsonValue::String(format!("#{r:02X}{g:02X}{b:02X}{a:02X}"))
116            }
117            Value::BigInt(value) => exact_i64_to_json(*value),
118            Value::KeyRef(collection, key) => JsonValue::Object(
119                [
120                    (
121                        "collection".to_string(),
122                        JsonValue::String(collection.clone()),
123                    ),
124                    ("key".to_string(), JsonValue::String(key.clone())),
125                ]
126                .into_iter()
127                .collect(),
128            ),
129            Value::DocRef(collection, id) => JsonValue::Object(
130                [
131                    (
132                        "collection".to_string(),
133                        JsonValue::String(collection.clone()),
134                    ),
135                    ("id".to_string(), exact_u64_to_json(*id)),
136                ]
137                .into_iter()
138                .collect(),
139            ),
140            Value::TableRef(name) => JsonValue::String(name.clone()),
141            Value::PageRef(page_id) => exact_u64_to_json(u64::from(*page_id)),
142            Value::Secret(_) | Value::Password(_) => JsonValue::String("***".to_string()),
143        }
144    }
145}
146
147fn exact_i64_to_json(value: i64) -> JsonValue {
148    if (-MAX_JSON_SAFE_INTEGER..=MAX_JSON_SAFE_INTEGER).contains(&value) {
149        JsonValue::Integer(value)
150    } else {
151        single_key_object("$int", JsonValue::String(value.to_string()))
152    }
153}
154
155fn exact_u64_to_json(value: u64) -> JsonValue {
156    if value <= MAX_JSON_SAFE_INTEGER as u64 {
157        JsonValue::Integer(value as i64)
158    } else {
159        single_key_object("$uint", JsonValue::String(value.to_string()))
160    }
161}
162
163fn exact_decimal_to_json(value: String) -> JsonValue {
164    single_key_object("$decimal", JsonValue::String(value))
165}
166
167fn single_key_object(key: &str, value: JsonValue) -> JsonValue {
168    JsonValue::Object([(key.to_string(), value)].into_iter().collect())
169}
170
171/// Decode a stored JSON payload into a JSON value, mirroring
172/// `Value::Json` rendering for callers that hold raw bytes.
173pub fn json_bytes_to_json(bytes: &[u8]) -> JsonValue {
174    if bytes.starts_with(crate::document_body_codec::MAGIC) {
175        if let Ok(fields) = crate::document_body_codec::decode(bytes) {
176            let mut object = Map::new();
177            for (key, value) in fields {
178                let json = match value {
179                    Value::Integer(value) => JsonValue::Integer(value),
180                    value => value.to_json(),
181                };
182                object.insert(key, json);
183            }
184            return JsonValue::Object(object);
185        }
186    }
187
188    json_from_slice::<JsonValue>(bytes).unwrap_or_else(|_| {
189        JsonValue::Object(
190            [
191                (
192                    "code".to_string(),
193                    JsonValue::String("INVALID_JSON".to_string()),
194                ),
195                ("hex".to_string(), JsonValue::String(hex_encode(bytes))),
196            ]
197            .into_iter()
198            .collect(),
199        )
200    })
201}
202
203fn format_mac(bytes: &[u8; 6]) -> String {
204    format!(
205        "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
206        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
207    )
208}
209
210fn format_ipv4(ip: u32) -> String {
211    format!(
212        "{}.{}.{}.{}",
213        (ip >> 24) & 0xFF,
214        (ip >> 16) & 0xFF,
215        (ip >> 8) & 0xFF,
216        ip & 0xFF
217    )
218}
219
220fn hex_encode(bytes: &[u8]) -> String {
221    const HEX: &[u8; 16] = b"0123456789abcdef";
222    let mut encoded = String::with_capacity(bytes.len() * 2);
223    for byte in bytes {
224        encoded.push(char::from(HEX[usize::from(*byte >> 4)]));
225        encoded.push(char::from(HEX[usize::from(*byte & 0x0f)]));
226    }
227    encoded
228}
229
230#[cfg(test)]
231mod tests {
232    use std::net::{IpAddr, Ipv6Addr};
233
234    use crate::Value;
235
236    #[test]
237    fn every_value_variant_has_canonical_json() {
238        let cases = [
239            ("null", Value::Null, "null"),
240            ("integer", Value::Integer(42), "42"),
241            (
242                "large integer",
243                Value::Integer(9_007_199_254_740_992),
244                r#"{"$int":"9007199254740992"}"#,
245            ),
246            ("unsigned integer", Value::UnsignedInteger(7), "7"),
247            (
248                "large unsigned integer",
249                Value::UnsignedInteger(u64::MAX),
250                r#"{"$uint":"18446744073709551615"}"#,
251            ),
252            ("float", Value::Float(1.5), "1.5"),
253            ("text", Value::text("hello"), r#""hello""#),
254            ("blob", Value::Blob(vec![0x00, 0xff]), r#""00ff""#),
255            ("boolean", Value::Boolean(true), "true"),
256            ("timestamp", Value::Timestamp(1_700_000_000), "1700000000"),
257            ("duration", Value::Duration(-500), "-500"),
258            (
259                "IP address",
260                Value::IpAddr(IpAddr::V6(Ipv6Addr::LOCALHOST)),
261                r#""::1""#,
262            ),
263            (
264                "MAC address",
265                Value::MacAddr([0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e]),
266                r#""00:1a:2b:3c:4d:5e""#,
267            ),
268            ("vector", Value::Vector(vec![1.5, -2.0]), "[1.5,-2]"),
269            (
270                "JSON",
271                Value::Json(br#"{"nested":[1,2.5],"text":"ok"}"#.to_vec()),
272                r#"{"nested":[1,2.5],"text":"ok"}"#,
273            ),
274            (
275                "invalid JSON",
276                Value::Json(vec![0xff]),
277                r#"{"code":"INVALID_JSON","hex":"ff"}"#,
278            ),
279            (
280                "UUID",
281                Value::Uuid([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]),
282                r#""000102030405060708090a0b0c0d0e0f""#,
283            ),
284            (
285                "node reference",
286                Value::NodeRef("node:1".into()),
287                r#""node:1""#,
288            ),
289            (
290                "edge reference",
291                Value::EdgeRef("edge:1".into()),
292                r#""edge:1""#,
293            ),
294            (
295                "vector reference",
296                Value::VectorRef("embeddings".into(), 7),
297                r#"{"collection":"embeddings","id":7}"#,
298            ),
299            (
300                "row reference",
301                Value::RowRef("users".into(), 8),
302                r#"{"row_id":8,"table":"users"}"#,
303            ),
304            ("color", Value::Color([0x12, 0xab, 0xff]), "\"#12ABFF\""),
305            ("email", Value::Email("a@b.test".into()), r#""a@b.test""#),
306            (
307                "URL",
308                Value::Url("https://example.test".into()),
309                r#""https://example.test""#,
310            ),
311            ("phone", Value::Phone(55_119_999), "55119999"),
312            ("semver", Value::Semver(1_002_003), r#""1.2.3""#),
313            ("CIDR", Value::Cidr(0xc0a8_0100, 24), r#""192.168.1.0/24""#),
314            ("date", Value::Date(20_000), "20000"),
315            ("time", Value::Time(3_723_000), "3723000"),
316            (
317                "decimal",
318                Value::Decimal(1_234_567),
319                r#"{"$decimal":"123.4567"}"#,
320            ),
321            (
322                "decimal text",
323                Value::DecimalText("3.141592653589793238462643383279".into()),
324                r#"{"$decimal":"3.141592653589793238462643383279"}"#,
325            ),
326            ("enum", Value::EnumValue(3), "3"),
327            (
328                "array",
329                Value::Array(vec![
330                    Value::Boolean(false),
331                    Value::Array(vec![Value::Integer(2)]),
332                ]),
333                "[false,[2]]",
334            ),
335            (
336                "millisecond timestamp",
337                Value::TimestampMs(1_700_000_000_123),
338                "1700000000123",
339            ),
340            ("IPv4", Value::Ipv4(0x7f00_0001), r#""127.0.0.1""#),
341            (
342                "IPv6",
343                Value::Ipv6(Ipv6Addr::LOCALHOST.octets()),
344                r#""::1""#,
345            ),
346            (
347                "subnet",
348                Value::Subnet(0x0a00_0000, 0xff00_0000),
349                r#""10.0.0.0/8""#,
350            ),
351            ("port", Value::Port(5432), "5432"),
352            ("latitude", Value::Latitude(12_345_678), "12.345678"),
353            ("longitude", Value::Longitude(-87_654_321), "-87.654321"),
354            (
355                "geo point",
356                Value::GeoPoint(12_345_678, -87_654_321),
357                r#""12.345678,-87.654321""#,
358            ),
359            ("country-2", Value::Country2(*b"BR"), r#""BR""#),
360            ("country-3", Value::Country3(*b"BRA"), r#""BRA""#),
361            ("language-2", Value::Lang2(*b"pt"), r#""pt""#),
362            ("language-5", Value::Lang5(*b"pt-BR"), r#""pt-BR""#),
363            ("currency", Value::Currency(*b"BRL"), r#""BRL""#),
364            ("asset code", Value::AssetCode("BTC".into()), r#""BTC""#),
365            (
366                "money",
367                Value::Money {
368                    asset_code: "BRL".into(),
369                    minor_units: 12_345,
370                    scale: 2,
371                },
372                r#"{"asset_code":"BRL","minor_units":12345,"scale":2}"#,
373            ),
374            (
375                "color with alpha",
376                Value::ColorAlpha([0x12, 0xab, 0xff, 0x80]),
377                "\"#12ABFF80\"",
378            ),
379            (
380                "big integer",
381                Value::BigInt(i64::MIN),
382                r#"{"$int":"-9223372036854775808"}"#,
383            ),
384            (
385                "key reference",
386                Value::KeyRef("settings".into(), "theme".into()),
387                r#"{"collection":"settings","key":"theme"}"#,
388            ),
389            (
390                "document reference",
391                Value::DocRef("profiles".into(), 9),
392                r#"{"collection":"profiles","id":9}"#,
393            ),
394            (
395                "table reference",
396                Value::TableRef("users".into()),
397                r#""users""#,
398            ),
399            ("page reference", Value::PageRef(17), "17"),
400            ("secret", Value::Secret(vec![1, 2, 3]), r#""***""#),
401            ("password", Value::Password("hash".into()), r#""***""#),
402        ];
403
404        for (name, value, expected) in cases {
405            assert_eq!(value.to_json().to_string_compact(), expected, "{name}");
406        }
407    }
408}