vantage_cli_util/output/
json.rs1use ciborium::Value as CborValue;
16use indexmap::IndexMap;
17use vantage_types::Record;
18
19pub fn write_list(records: &IndexMap<String, Record<CborValue>>) -> String {
20 let mut out = String::from("{");
21 let mut first = true;
22 for (id, record) in records {
23 if !first {
24 out.push(',');
25 }
26 first = false;
27 out.push_str(&write_string(id));
28 out.push(':');
29 out.push_str(&write_record_map(record));
30 }
31 out.push('}');
32 out.push('\n');
33 out
34}
35
36pub fn write_record(id: &str, record: &Record<CborValue>) -> String {
37 let mut out = String::from("{");
38 out.push_str(&write_string(id));
39 out.push(':');
40 out.push_str(&write_record_map(record));
41 out.push('}');
42 out.push('\n');
43 out
44}
45
46pub fn write_scalar(label: &str, value: &CborValue) -> String {
47 let mut out = String::from("{");
48 out.push_str(&write_string(label));
49 out.push(':');
50 out.push_str(&write_value(value));
51 out.push('}');
52 out.push('\n');
53 out
54}
55
56pub fn write_record_map(record: &Record<CborValue>) -> String {
57 let mut out = String::from("{");
58 let mut first = true;
59 for (k, v) in record.iter() {
60 if !first {
61 out.push(',');
62 }
63 first = false;
64 out.push_str(&write_string(k));
65 out.push(':');
66 out.push_str(&write_value(v));
67 }
68 out.push('}');
69 out
70}
71
72pub fn write_value(v: &CborValue) -> String {
73 match v {
74 CborValue::Integer(i) => write_integer(*i),
75 CborValue::Float(f) => write_float(*f),
76 CborValue::Text(s) => write_string(s),
77 CborValue::Bytes(b) => write_bytes(b),
78 CborValue::Bool(b) => b.to_string(),
79 CborValue::Null => "null".to_string(),
80 CborValue::Array(items) => write_array(items),
81 CborValue::Map(pairs) => write_map(pairs),
82 CborValue::Tag(_, inner) => write_value(inner),
83 other => write_string(&format!("{other:?}")),
84 }
85}
86
87fn write_integer(i: ciborium::value::Integer) -> String {
91 let n = i128::from(i);
92 if (i64::MIN as i128..=i64::MAX as i128).contains(&n) {
93 n.to_string()
94 } else {
95 format!("\"{n}\"")
96 }
97}
98
99fn write_float(f: f64) -> String {
101 if f.is_nan() || f.is_infinite() {
102 "null".to_string()
103 } else {
104 f.to_string()
105 }
106}
107
108fn write_bytes(b: &[u8]) -> String {
110 let mut hex = String::with_capacity(b.len() * 2 + 4);
111 hex.push_str("\"0x");
112 for byte in b {
113 hex.push_str(&format!("{byte:02x}"));
114 }
115 hex.push('"');
116 hex
117}
118
119fn write_array(items: &[CborValue]) -> String {
120 let mut out = String::from("[");
121 for (i, item) in items.iter().enumerate() {
122 if i > 0 {
123 out.push(',');
124 }
125 out.push_str(&write_value(item));
126 }
127 out.push(']');
128 out
129}
130
131fn write_map(pairs: &[(CborValue, CborValue)]) -> String {
132 let mut out = String::from("{");
133 for (i, (k, v)) in pairs.iter().enumerate() {
134 if i > 0 {
135 out.push(',');
136 }
137 out.push_str(&write_map_key(k));
138 out.push(':');
139 out.push_str(&write_value(v));
140 }
141 out.push('}');
142 out
143}
144
145fn write_map_key(k: &CborValue) -> String {
148 match k {
149 CborValue::Text(s) => write_string(s),
150 other => write_string(&write_value(other)),
151 }
152}
153
154fn write_string(s: &str) -> String {
155 serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn scalar_kinds() {
166 assert_eq!(write_value(&CborValue::Integer(42.into())), "42");
167 assert_eq!(write_value(&CborValue::Integer((-7).into())), "-7");
168 assert_eq!(write_value(&CborValue::Bool(true)), "true");
169 assert_eq!(write_value(&CborValue::Null), "null");
170 assert_eq!(write_value(&CborValue::Text("hi".into())), "\"hi\"");
171 }
172
173 #[test]
174 fn float_specials_become_null() {
175 assert_eq!(write_value(&CborValue::Float(f64::NAN)), "null");
176 assert_eq!(write_value(&CborValue::Float(f64::INFINITY)), "null");
177 }
178
179 #[test]
180 fn bytes_are_hex_strings() {
181 assert_eq!(
182 write_value(&CborValue::Bytes(vec![0xab, 0xcd])),
183 "\"0xabcd\""
184 );
185 }
186
187 #[test]
188 fn record_framing() {
189 let mut r = Record::new();
190 r.insert("name".to_string(), CborValue::Text("alice".into()));
191 r.insert("age".to_string(), CborValue::Integer(30.into()));
192 let s = write_record("u1", &r);
193 assert_eq!(s, "{\"u1\":{\"name\":\"alice\",\"age\":30}}\n");
194 }
195
196 #[test]
197 fn list_framing() {
198 let mut records: IndexMap<String, Record<CborValue>> = IndexMap::new();
199 let mut r1 = Record::new();
200 r1.insert("x".to_string(), CborValue::Integer(1.into()));
201 records.insert("a".to_string(), r1);
202 assert_eq!(write_list(&records), "{\"a\":{\"x\":1}}\n");
203 }
204}