use ciborium::Value as CborValue;
use indexmap::IndexMap;
use vantage_types::Record;
pub fn write_list(records: &IndexMap<String, Record<CborValue>>) -> String {
let mut out = String::from("{");
let mut first = true;
for (id, record) in records {
if !first {
out.push(',');
}
first = false;
out.push_str(&write_string(id));
out.push(':');
out.push_str(&write_record_map(record));
}
out.push('}');
out.push('\n');
out
}
pub fn write_record(id: &str, record: &Record<CborValue>) -> String {
let mut out = String::from("{");
out.push_str(&write_string(id));
out.push(':');
out.push_str(&write_record_map(record));
out.push('}');
out.push('\n');
out
}
pub fn write_scalar(label: &str, value: &CborValue) -> String {
let mut out = String::from("{");
out.push_str(&write_string(label));
out.push(':');
out.push_str(&write_value(value));
out.push('}');
out.push('\n');
out
}
pub fn write_record_map(record: &Record<CborValue>) -> String {
let mut out = String::from("{");
let mut first = true;
for (k, v) in record.iter() {
if !first {
out.push(',');
}
first = false;
out.push_str(&write_string(k));
out.push(':');
out.push_str(&write_value(v));
}
out.push('}');
out
}
pub fn write_value(v: &CborValue) -> String {
match v {
CborValue::Integer(i) => write_integer(*i),
CborValue::Float(f) => write_float(*f),
CborValue::Text(s) => write_string(s),
CborValue::Bytes(b) => write_bytes(b),
CborValue::Bool(b) => b.to_string(),
CborValue::Null => "null".to_string(),
CborValue::Array(items) => write_array(items),
CborValue::Map(pairs) => write_map(pairs),
CborValue::Tag(_, inner) => write_value(inner),
other => write_string(&format!("{other:?}")),
}
}
fn write_integer(i: ciborium::value::Integer) -> String {
let n = i128::from(i);
if (i64::MIN as i128..=i64::MAX as i128).contains(&n) {
n.to_string()
} else {
format!("\"{n}\"")
}
}
fn write_float(f: f64) -> String {
if f.is_nan() || f.is_infinite() {
"null".to_string()
} else {
f.to_string()
}
}
fn write_bytes(b: &[u8]) -> String {
let mut hex = String::with_capacity(b.len() * 2 + 4);
hex.push_str("\"0x");
for byte in b {
hex.push_str(&format!("{byte:02x}"));
}
hex.push('"');
hex
}
fn write_array(items: &[CborValue]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&write_value(item));
}
out.push(']');
out
}
fn write_map(pairs: &[(CborValue, CborValue)]) -> String {
let mut out = String::from("{");
for (i, (k, v)) in pairs.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&write_map_key(k));
out.push(':');
out.push_str(&write_value(v));
}
out.push('}');
out
}
fn write_map_key(k: &CborValue) -> String {
match k {
CborValue::Text(s) => write_string(s),
other => write_string(&write_value(other)),
}
}
fn write_string(s: &str) -> String {
serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalar_kinds() {
assert_eq!(write_value(&CborValue::Integer(42.into())), "42");
assert_eq!(write_value(&CborValue::Integer((-7).into())), "-7");
assert_eq!(write_value(&CborValue::Bool(true)), "true");
assert_eq!(write_value(&CborValue::Null), "null");
assert_eq!(write_value(&CborValue::Text("hi".into())), "\"hi\"");
}
#[test]
fn float_specials_become_null() {
assert_eq!(write_value(&CborValue::Float(f64::NAN)), "null");
assert_eq!(write_value(&CborValue::Float(f64::INFINITY)), "null");
}
#[test]
fn bytes_are_hex_strings() {
assert_eq!(
write_value(&CborValue::Bytes(vec![0xab, 0xcd])),
"\"0xabcd\""
);
}
#[test]
fn record_framing() {
let mut r = Record::new();
r.insert("name".to_string(), CborValue::Text("alice".into()));
r.insert("age".to_string(), CborValue::Integer(30.into()));
let s = write_record("u1", &r);
assert_eq!(s, "{\"u1\":{\"name\":\"alice\",\"age\":30}}\n");
}
#[test]
fn list_framing() {
let mut records: IndexMap<String, Record<CborValue>> = IndexMap::new();
let mut r1 = Record::new();
r1.insert("x".to_string(), CborValue::Integer(1.into()));
records.insert("a".to_string(), r1);
assert_eq!(write_list(&records), "{\"a\":{\"x\":1}}\n");
}
}