Skip to main content

vantage_cli_util/output/
ndjson.rs

1//! Newline-delimited JSON — one record per line, easy to pipe to `jq -c`
2//! or read line-by-line in tests. Same lossy rules as `json` (see that
3//! module).
4
5use ciborium::Value as CborValue;
6use indexmap::IndexMap;
7use vantage_types::Record;
8
9use super::json;
10
11pub fn write_list(records: &IndexMap<String, Record<CborValue>>) -> String {
12    let mut out = String::new();
13    for (id, record) in records {
14        out.push_str(&line(id, record));
15    }
16    out
17}
18
19pub fn write_record(id: &str, record: &Record<CborValue>) -> String {
20    line(id, record)
21}
22
23pub fn write_scalar(label: &str, value: &CborValue) -> String {
24    let mut out = String::from("{");
25    out.push_str(&serde_json::to_string(label).unwrap_or_else(|_| "\"\"".to_string()));
26    out.push(':');
27    out.push_str(&json::write_value(value));
28    out.push_str("}\n");
29    out
30}
31
32fn line(id: &str, record: &Record<CborValue>) -> String {
33    let mut out = String::from("{");
34    // The IndexMap key is reported under `_id` (sentinel-prefixed) to
35    // sidestep clashes with the record's own `id` field. Records
36    // typically carry their own `id` which is the authoritative typed
37    // value; `_id` is only a stable framing handle.
38    out.push_str(&serde_json::to_string("_id").unwrap_or_else(|_| "\"_id\"".to_string()));
39    out.push(':');
40    out.push_str(&serde_json::to_string(id).unwrap_or_else(|_| "\"\"".to_string()));
41    for (k, v) in record.iter() {
42        out.push(',');
43        out.push_str(&serde_json::to_string(k).unwrap_or_else(|_| "\"\"".to_string()));
44        out.push(':');
45        out.push_str(&json::write_value(v));
46    }
47    out.push_str("}\n");
48    out
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn one_line_per_record() {
57        let mut records: IndexMap<String, Record<CborValue>> = IndexMap::new();
58        let mut r1 = Record::new();
59        r1.insert("x".to_string(), CborValue::Integer(1.into()));
60        records.insert("a".to_string(), r1);
61        let mut r2 = Record::new();
62        r2.insert("x".to_string(), CborValue::Integer(2.into()));
63        records.insert("b".to_string(), r2);
64        let s = write_list(&records);
65        let lines: Vec<&str> = s.lines().collect();
66        assert_eq!(lines.len(), 2);
67        assert_eq!(lines[0], "{\"_id\":\"a\",\"x\":1}");
68        assert_eq!(lines[1], "{\"_id\":\"b\",\"x\":2}");
69    }
70
71    #[test]
72    fn scalar_one_line() {
73        let s = write_scalar("count", &CborValue::Integer(5.into()));
74        assert_eq!(s, "{\"count\":5}\n");
75    }
76}