Skip to main content

substrate_data/forms/text/
writer.rs

1//! Text out of a value: compact or indented, typed values in their string forms.
2
3use crate::model::value::{Map, Value};
4
5/// `value` as compact text (typed values in their string forms).
6pub fn to_string(value: &Value) -> String {
7    let mut out = String::with_capacity(256);
8    write(value, None, 0, &mut out);
9    out
10}
11
12/// `value` as text indented by two spaces, one entry per line.
13pub fn to_string_pretty(value: &Value) -> String {
14    let mut out = String::new();
15    write(value, Some("  "), 0, &mut out);
16    out.push('\n');
17    out
18}
19
20fn write(value: &Value, indent: Option<&str>, level: usize, out: &mut String) {
21    Layout { indent, out }.value(value, level);
22}
23
24/// Text being written, and the indent unit if it's pretty.
25struct Layout<'a, 'o> {
26    indent: Option<&'a str>,
27    out: &'o mut String,
28}
29
30impl Layout<'_, '_> {
31    fn value(&mut self, value: &Value, level: usize) {
32        match value {
33            Value::Null => self.out.push_str("null"),
34            Value::Bool(b) => self.out.push_str(if *b { "true" } else { "false" }),
35            Value::Int(n) => push_fmt(self.out, format_args!("{n}")),
36            Value::Float(f) => write_float(*f, self.out),
37            Value::List(items) => self.list(items, level),
38            Value::Map(map) => self.map(map, level),
39            // As spelled: a plain string's leading `@` was doubled when it
40            // was put in the value (`Schema::write`), a typed one's wasn't.
41            Value::Str(s) => write_string(s, self.out),
42            typed => write_string(&crate::typed::spelling(typed).unwrap_or_default(), self.out),
43        }
44    }
45
46    fn newline(&mut self, level: usize) {
47        if let Some(unit) = self.indent {
48            self.out.push('\n');
49            for _ in 0..level {
50                self.out.push_str(unit);
51            }
52        }
53    }
54
55    fn list(&mut self, items: &[Value], level: usize) {
56        if items.is_empty() {
57            self.out.push_str("[]");
58            return;
59        }
60        self.out.push('[');
61        for (i, v) in items.iter().enumerate() {
62            if i > 0 {
63                self.out.push(',');
64            }
65            self.newline(level + 1);
66            self.value(v, level + 1);
67        }
68        self.newline(level);
69        self.out.push(']');
70    }
71
72    fn map(&mut self, map: &Map, level: usize) {
73        if map.is_empty() {
74            self.out.push_str("{}");
75            return;
76        }
77        self.out.push('{');
78        for (i, (k, v)) in map.iter().enumerate() {
79            if i > 0 {
80                self.out.push(',');
81            }
82            self.newline(level + 1);
83            write_string(k, self.out);
84            self.out
85                .push_str(if self.indent.is_some() { ": " } else { ":" });
86            self.value(v, level + 1);
87        }
88        self.newline(level);
89        self.out.push('}');
90    }
91}
92
93/// Formats into `out`; writing to a `String` can't fail.
94fn push_fmt(out: &mut String, args: std::fmt::Arguments<'_>) {
95    use std::fmt::Write as _;
96    out.write_fmt(args).unwrap_or_default();
97}
98
99fn write_float(f: f64, out: &mut String) {
100    if f.is_finite() {
101        out.push_str(&float_string(f));
102    } else {
103        out.push_str("null");
104    }
105}
106
107/// A finite float in the shortest spelling that reads back as exactly it,
108/// and as a float: `1.0`, `0.1`, `1e-7`, `1e16` (never `10000000000000000`,
109/// which would read back as an integer).
110pub(crate) fn float_string(f: f64) -> String {
111    format!("{f:?}")
112}
113
114/// `s` as a JSON string literal.
115pub fn write_string(s: &str, out: &mut String) {
116    out.push('"');
117    write_string_body(s, out);
118    out.push('"');
119}
120
121fn write_string_body(s: &str, out: &mut String) {
122    let bytes = s.as_bytes();
123    let mut run = 0;
124    while let Some(n) = bytes[run..]
125        .iter()
126        .position(|&b| b == b'"' || b == b'\\' || b < 0x20)
127    {
128        let i = run + n;
129        out.push_str(&s[run..i]);
130        match bytes[i] {
131            b'"' => out.push_str("\\\""),
132            b'\\' => out.push_str("\\\\"),
133            b'\n' => out.push_str("\\n"),
134            b'\r' => out.push_str("\\r"),
135            b'\t' => out.push_str("\\t"),
136            b => push_fmt(out, format_args!("\\u{b:04x}")),
137        }
138        run = i + 1;
139    }
140    out.push_str(&s[run..]);
141}