workflow_i18n/
json.rs

1// adopted from `sorted-json` crate
2
3use itertools;
4
5pub fn to_json(v: &serde_json::Value) -> String {
6    let s = String::new();
7    to_json_(v, s, "")
8}
9
10fn escape_json_string(out: &mut String, s: &str) {
11    out.push('\"');
12
13    let bytes = s.as_bytes();
14
15    let mut start = 0;
16
17    for (i, &byte) in bytes.iter().enumerate() {
18        let escape = ESCAPE[byte as usize];
19        if escape == 0 {
20            continue;
21        }
22
23        if start < i {
24            out.push_str(&s[start..i]);
25        }
26
27        let char_escape = CharEscape::from_escape_table(escape, byte);
28        out.push_str(&write_char_escape(char_escape));
29
30        start = i + 1;
31    }
32
33    if start != bytes.len() {
34        out.push_str(&s[start..]);
35    }
36
37    out.push('\"');
38}
39
40const TAB: &str = "  ";
41
42fn to_json_(v: &serde_json::Value, mut out: String, prefix: &str) -> String {
43    // pretty printer for json that prints the dict keys in sorted order
44    let prefix2 = format!("{}{}", prefix, TAB);
45    match v {
46        serde_json::Value::String(s) => escape_json_string(&mut out, s),
47        serde_json::Value::Null => out.push_str("null"),
48        serde_json::Value::Bool(b) => {
49            if *b {
50                out.push_str("true")
51            } else {
52                out.push_str("false")
53            }
54        }
55        serde_json::Value::Number(n) => out.push_str(&format!("{}", n)),
56        serde_json::Value::Array(a) => {
57            let len = a.len();
58            if len == 0 {
59                out.push_str("[]");
60            } else {
61                out.push_str("[\n");
62                for (idx, item) in itertools::enumerate(a.iter()) {
63                    out.push_str(&prefix2);
64                    out = to_json_(item, out, &prefix2);
65                    if idx < len - 1 {
66                        out.push_str(",\n");
67                    }
68                }
69                out.push('\n');
70                out.push_str(prefix);
71                out.push(']');
72            }
73        }
74        serde_json::Value::Object(m) => {
75            let len = m.len();
76            if len == 0 {
77                out.push_str("{}");
78            } else {
79                out.push_str("{\n");
80                for (idx, k) in itertools::enumerate(itertools::sorted(m.keys())) {
81                    let v = m.get(k).unwrap();
82                    out.push_str(&prefix2);
83                    escape_json_string(&mut out, k);
84                    out.push_str(": ");
85                    out = to_json_(v, out, &prefix2);
86                    if idx < len - 1 {
87                        out.push_str(",\n");
88                    }
89                }
90                out.push('\n');
91                out.push_str(prefix);
92                out.push('}');
93            }
94        }
95    }
96    out
97}
98
99const BB: u8 = b'b'; // \x08
100const TT: u8 = b't'; // \x09
101const NN: u8 = b'n'; // \x0A
102const FF: u8 = b'f'; // \x0C
103const RR: u8 = b'r'; // \x0D
104const QU: u8 = b'"'; // \x22
105const BS: u8 = b'\\'; // \x5C
106const U: u8 = b'u'; // \x00...\x1F except the ones above
107
108// Lookup table of escape sequences. A value of b'x' at index i means that byte
109// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.
110#[rustfmt::skip]
111static ESCAPE: [u8; 256] = [
112    //  1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
113    U,  U,  U,  U,  U,  U,  U,  U, BB, TT, NN,  U, FF, RR,  U,  U, // 0
114    U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U, // 1
115    0,  0, QU,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 2
116    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 3
117    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 4
118    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, BS,  0,  0,  0, // 5
119    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 6
120    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 7
121    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 8
122    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 9
123    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // A
124    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // B
125    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // C
126    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // D
127    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // E
128    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // F
129];
130
131/// Represents a character escape code in a type-safe manner.
132pub enum CharEscape {
133    /// An escaped quote `"`
134    Quote,
135    /// An escaped reverse solidus `\`
136    ReverseSolidus,
137    /// An escaped solidus `/`
138    Solidus,
139    /// An escaped backspace character (usually escaped as `\b`)
140    Backspace,
141    /// An escaped form feed character (usually escaped as `\f`)
142    FormFeed,
143    /// An escaped line feed character (usually escaped as `\n`)
144    LineFeed,
145    /// An escaped carriage return character (usually escaped as `\r`)
146    CarriageReturn,
147    /// An escaped tab character (usually escaped as `\t`)
148    Tab,
149    /// An escaped ASCII plane control character (usually escaped as
150    /// `\u00XX` where `XX` are two hex characters)
151    AsciiControl(u8),
152}
153
154impl CharEscape {
155    #[inline]
156    fn from_escape_table(escape: u8, byte: u8) -> CharEscape {
157        match escape {
158            self::BB => CharEscape::Backspace,
159            self::TT => CharEscape::Tab,
160            self::NN => CharEscape::LineFeed,
161            self::FF => CharEscape::FormFeed,
162            self::RR => CharEscape::CarriageReturn,
163            self::QU => CharEscape::Quote,
164            self::BS => CharEscape::ReverseSolidus,
165            self::U => CharEscape::AsciiControl(byte),
166            _ => unreachable!(),
167        }
168    }
169}
170
171#[inline]
172fn write_char_escape(char_escape: CharEscape) -> String {
173    use self::CharEscape::*;
174
175    let mut out: Vec<u8> = vec![];
176    match char_escape {
177        Quote => out.extend(b"\\\""),
178        ReverseSolidus => out.extend(b"\\\\"),
179        Solidus => out.extend(b"\\/"),
180        Backspace => out.extend(b"\\b"),
181        FormFeed => out.extend(b"\\f"),
182        LineFeed => out.extend(b"\\n"),
183        CarriageReturn => out.extend(b"\\r"),
184        Tab => out.extend(b"\\t"),
185        AsciiControl(byte) => {
186            static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
187            let bytes = &[
188                b'\\',
189                b'u',
190                b'0',
191                b'0',
192                HEX_DIGITS[(byte >> 4) as usize],
193                HEX_DIGITS[(byte & 0xF) as usize],
194            ];
195            out.extend(bytes);
196        }
197    };
198    String::from_utf8(out).unwrap()
199}
200
201#[cfg(test)]
202mod tests {
203    use serde_json::{json, value::Number, Value};
204
205    fn a(v: Value, out: &'static str) {
206        assert_eq!(super::to_json(&v), out);
207    }
208
209    #[test]
210    fn to_json() {
211        a(json! {null}, "null");
212        a(json! {1}, "1");
213        a(Value::Number(Number::from_f64(1.0).unwrap()), "1.0");
214        a(
215            Value::Number(Number::from_f64(-1.0002300e2).unwrap()),
216            "-100.023",
217        );
218        a(json! {"foo"}, "\"foo\"");
219        a(json! {r#"hello "world""#}, r#""hello \"world\"""#);
220        a(
221            json! {[1, 2]},
222            "[
223  1,
224  2
225]",
226        );
227        a(
228            json! {[1, 2, []]},
229            "[
230  1,
231  2,
232  []
233]",
234        );
235        a(
236            json! {[1, 2, [1, 2, [1, 2]]]},
237            "[
238  1,
239  2,
240  [
241    1,
242    2,
243    [
244      1,
245      2
246    ]
247  ]
248]",
249        );
250        a(
251            json! {{"yo": 1, "lo": 2, "no": {}}},
252            "{
253  \"lo\": 2,
254  \"no\": {},
255  \"yo\": 1
256}",
257        );
258        a(
259            json! {{"yo": 1, "lo": 2, "baz": {"one": 1, "do": 2, "tres": {"x": "x", "y": "y"}}}},
260            "{
261  \"baz\": {
262    \"do\": 2,
263    \"one\": 1,
264    \"tres\": {
265      \"x\": \"x\",
266      \"y\": \"y\"
267    }
268  },
269  \"lo\": 2,
270  \"yo\": 1
271}",
272        );
273        a(
274            json! {{"yo": 1, "lo": 2, "baz": {"one": 1, "do": 2, "tres": ["x", "x", "y", "y"]}}},
275            "{
276  \"baz\": {
277    \"do\": 2,
278    \"one\": 1,
279    \"tres\": [
280      \"x\",
281      \"x\",
282      \"y\",
283      \"y\"
284    ]
285  },
286  \"lo\": 2,
287  \"yo\": 1
288}",
289        );
290    }
291}