LCOV - code coverage report
Current view: top level - dashql-core/src - compact_json.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 98.9 % 89 88
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : //! Print JSON compactly within a line width, e.g. the Vega-Lite specs of `@vis` tags.
       2              : //!
       3              : //! A value that fits in the rest of its line stays on one line, with a space after `:` and `,`. Objects
       4              : //! and arrays that don't fit put each entry on a line of its own, indented by 2 spaces, and try again
       5              : //! for every entry. Keys keep their order. Scalars are never split, so long strings can exceed the
       6              : //! width.
       7              : 
       8              : use serde_json::Value;
       9              : 
      10              : const INDENT: &str = "  ";
      11              : 
      12              : /// Print a value compactly, its lines are at most `width` wide if the scalars allow it
      13           86 : pub fn to_string_compact(value: &Value, width: usize) -> String {
      14           86 :     let mut out = String::new();
      15           86 :     write_value(&mut out, value, "", 0, width);
      16           86 :     out
      17           86 : }
      18              : 
      19              : /// The value on one line
      20          892 : fn one_line(value: &Value) -> String {
      21          892 :     match value {
      22           30 :         Value::Array(items) => format!("[{}]", items.iter().map(one_line).collect::<Vec<_>>().join(", ")),
      23          383 :         Value::Object(entries) => {
      24          605 :             let entries: Vec<String> = entries.iter().map(|(k, v)| format!("{}: {}", key(k), one_line(v))).collect();
      25          383 :             format!("{{{}}}", entries.join(", "))
      26              :         }
      27          479 :         _ => value.to_string(),
      28              :     }
      29          892 : }
      30              : 
      31          724 : fn key(key: &str) -> String {
      32          724 :     Value::String(key.to_string()).to_string()
      33          724 : }
      34              : 
      35              : /// Write a value at the indentation `indent`, `taken` is the width of its line without the value: the
      36              : /// key in front of it and the comma after it
      37          229 : fn write_value(out: &mut String, value: &Value, indent: &str, taken: usize, width: usize) {
      38          229 :     let line = one_line(value);
      39          229 :     let breakable = match value {
      40           17 :         Value::Array(items) => !items.is_empty(),
      41          156 :         Value::Object(entries) => !entries.is_empty(),
      42           56 :         _ => false,
      43              :     };
      44          229 :     if !breakable || indent.len() + taken + line.len() <= width {
      45          153 :         out.push_str(&line);
      46          153 :         return;
      47           76 :     }
      48           76 :     let inner = format!("{indent}{INDENT}");
      49          143 :     let entry = |out: &mut String, last: bool| {
      50          143 :         out.push('\n');
      51          143 :         out.push_str(&inner);
      52          143 :         usize::from(!last)
      53          143 :     };
      54           76 :     match value {
      55           12 :         Value::Array(items) => {
      56           12 :             out.push('[');
      57           24 :             for (i, item) in items.iter().enumerate() {
      58           24 :                 let last = i + 1 == items.len();
      59           24 :                 let comma = entry(out, last);
      60           24 :                 write_value(out, item, &inner, comma, width);
      61           24 :                 if !last {
      62           12 :                     out.push(',');
      63           12 :                 }
      64              :             }
      65           12 :             out.push('\n');
      66           12 :             out.push_str(indent);
      67           12 :             out.push(']');
      68              :         }
      69           64 :         Value::Object(entries) => {
      70           64 :             out.push('{');
      71          119 :             for (i, (k, v)) in entries.iter().enumerate() {
      72          119 :                 let last = i + 1 == entries.len();
      73          119 :                 let comma = entry(out, last);
      74          119 :                 let k = key(k);
      75          119 :                 out.push_str(&k);
      76          119 :                 out.push_str(": ");
      77          119 :                 write_value(out, v, &inner, k.len() + 2 + comma, width);
      78          119 :                 if !last {
      79           55 :                     out.push(',');
      80           70 :                 }
      81              :             }
      82           64 :             out.push('\n');
      83           64 :             out.push_str(indent);
      84           64 :             out.push('}');
      85              :         }
      86            0 :         _ => unreachable!("scalars fit on one line"),
      87              :     }
      88          229 : }
      89              : 
      90              : #[cfg(test)]
      91              : mod tests {
      92              :     use super::*;
      93              : 
      94           16 :     fn compact(json: &str, width: usize) -> String {
      95           16 :         to_string_compact(&serde_json::from_str(json).unwrap(), width)
      96           16 :     }
      97              : 
      98              :     #[test]
      99            1 :     fn keeps_values_that_fit_on_one_line() {
     100            1 :         assert_eq!(compact(r#"{"mark":"bar","x":[1,2.5,null,true]}"#, 80), r#"{"mark": "bar", "x": [1, 2.5, null, true]}"#);
     101            1 :         assert_eq!(compact("[]", 0), "[]");
     102            1 :         assert_eq!(compact("{}", 0), "{}");
     103            1 :         assert_eq!(compact(r#""a\"b""#, 0), r#""a\"b""#);
     104            1 :     }
     105              : 
     106              :     #[test]
     107            1 :     fn breaks_what_does_not_fit() {
     108            1 :         let spec = r#"{"mark": {"type": "line", "point": true}, "encoding": {"x": {"field": "date", "type": "temporal"}, "y": {"field": "revenue", "aggregate": "sum"}}}"#;
     109            1 :         assert_eq!(
     110            1 :             compact(spec, 50),
     111              :             r#"{
     112              :   "mark": {"type": "line", "point": true},
     113              :   "encoding": {
     114              :     "x": {"field": "date", "type": "temporal"},
     115              :     "y": {"field": "revenue", "aggregate": "sum"}
     116              :   }
     117              : }"#
     118              :         );
     119              :         // The key and the comma count, `"x": ...,` is 46 wide
     120            1 :         assert_eq!(compact(r#"[{"x": {"field": "date", "type": "temporal"}}, 1]"#, 47), "[\n  {\"x\": {\"field\": \"date\", \"type\": \"temporal\"}},\n  1\n]");
     121            1 :         assert_eq!(
     122            1 :             compact(r#"[{"x": {"field": "date", "type": "temporal"}}, 1]"#, 46),
     123              :             "[\n  {\n    \"x\": {\"field\": \"date\", \"type\": \"temporal\"}\n  },\n  1\n]"
     124              :         );
     125            1 :     }
     126              : 
     127              :     #[test]
     128            1 :     fn keeps_the_key_order() {
     129            1 :         assert_eq!(compact(r#"{"b": 1, "a": 2}"#, 80), r#"{"b": 1, "a": 2}"#);
     130            1 :     }
     131              : 
     132              :     #[test]
     133            1 :     fn prints_what_it_reads() {
     134            1 :         let spec = r#"{"layer": [{"mark": "bar"}, {"mark": {"type": "text", "dy": -5}, "encoding": {"text": {"field": "a b"}}}], "width": 300}"#;
     135            4 :         for width in [0, 20, 40, 80] {
     136            4 :             let once = compact(spec, width);
     137            4 :             assert_eq!(serde_json::from_str::<Value>(&once).unwrap(), serde_json::from_str::<Value>(spec).unwrap());
     138            4 :             assert_eq!(compact(&once, width), once);
     139              :         }
     140            1 :     }
     141              : }
        

Generated by: LCOV version 2.0-1