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 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
171pub 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}