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 : }
|