use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use ciborium::Value as CborValue;
use serde_json::{Number, Value};
use vantage_types::cbor_json::{self, CborDialect};
pub(crate) struct SurrealJsonDialect;
impl CborDialect for SurrealJsonDialect {
fn bytes_to_json(&self, bytes: Vec<u8>) -> Value {
Value::String(BASE64.encode(bytes))
}
fn big_int_to_json(&self, n: i128) -> Value {
Number::from_f64(n as f64).map_or(Value::Null, Value::Number)
}
fn tag_to_json(&self, tag: u64, inner: CborValue) -> Value {
if tag == 8
&& let CborValue::Array(parts) = &inner
&& let [CborValue::Text(table), id] = parts.as_slice()
{
return match id {
CborValue::Text(id) => Value::String(format!("{table}:{id}")),
other => Value::String(format!(
"{table}:{}",
cbor_json::cbor_to_string(self, other)
)),
};
}
cbor_json::cbor_to_json(self, inner)
}
}
pub(crate) fn json_to_cbor(value: Value) -> CborValue {
cbor_json::json_to_cbor(value)
}
pub(crate) fn cbor_to_json(value: CborValue) -> Value {
cbor_json::cbor_to_json(&SurrealJsonDialect, value)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn roundtrip(v: Value) -> Value {
cbor_to_json(json_to_cbor(v))
}
#[test]
fn primitives_round_trip() {
assert_eq!(roundtrip(Value::Null), Value::Null);
assert_eq!(roundtrip(json!(true)), json!(true));
assert_eq!(roundtrip(json!("hello")), json!("hello"));
assert_eq!(roundtrip(json!(42)), json!(42));
assert_eq!(roundtrip(json!(-7)), json!(-7));
assert_eq!(roundtrip(json!(3.5)), json!(3.5));
}
#[test]
fn nested_round_trip() {
let v = json!({
"id": "users:john",
"tags": ["a", "b", "c"],
"meta": { "active": true, "count": 12 }
});
assert_eq!(roundtrip(v.clone()), v);
}
#[test]
fn surreal_query_response_shape() {
let v = json!([
{ "status": "OK", "result": [{ "id": "users:1", "name": "Alice" }] }
]);
assert_eq!(roundtrip(v.clone()), v);
}
#[test]
fn numeric_edges() {
assert_eq!(roundtrip(json!(i64::MAX)), json!(i64::MAX));
assert_eq!(roundtrip(json!(i64::MIN)), json!(i64::MIN));
assert_eq!(roundtrip(json!(u64::MAX)), json!(u64::MAX));
}
#[test]
fn nan_and_inf_become_null() {
assert_eq!(cbor_to_json(CborValue::Float(f64::NAN)), Value::Null);
assert_eq!(cbor_to_json(CborValue::Float(f64::INFINITY)), Value::Null);
}
#[test]
fn cbor_bytes_become_base64() {
let cbor = CborValue::Bytes(vec![0x68, 0x69]);
assert_eq!(cbor_to_json(cbor), Value::String("aGk=".to_string()));
}
#[test]
fn cbor_tag_unwraps_to_inner() {
let cbor = CborValue::Tag(0, Box::new(CborValue::Text("2024-01-01".to_string())));
assert_eq!(cbor_to_json(cbor), Value::String("2024-01-01".to_string()));
}
#[test]
fn record_id_becomes_table_colon_id() {
let cbor = CborValue::Tag(
8,
Box::new(CborValue::Array(vec![
CborValue::Text("users".into()),
CborValue::Text("john".into()),
])),
);
assert_eq!(cbor_to_json(cbor), Value::String("users:john".to_string()));
}
#[test]
fn numeric_record_id_stringifies() {
let cbor = CborValue::Tag(
8,
Box::new(CborValue::Array(vec![
CborValue::Text("users".into()),
CborValue::Integer(42.into()),
])),
);
assert_eq!(cbor_to_json(cbor), Value::String("users:42".to_string()));
}
}