1pub fn escape(s: &str) -> String {
11 let mut out = String::with_capacity(s.len() + 2);
12 for c in s.chars() {
13 match c {
14 '"' => out.push_str("\\\""),
15 '\\' => out.push_str("\\\\"),
16 '\n' => out.push_str("\\n"),
17 '\r' => out.push_str("\\r"),
18 '\t' => out.push_str("\\t"),
19 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
20 c => out.push(c),
21 }
22 }
23 out
24}
25
26pub fn string_field(key: &str, value: &str) -> String {
27 format!("\"{}\":\"{}\"", escape(key), escape(value))
28}
29
30pub fn bool_field(key: &str, value: bool) -> String {
31 format!("\"{}\":{value}", escape(key))
32}
33
34pub fn int_field(key: &str, value: i64) -> String {
37 format!("\"{}\":{value}", escape(key))
38}
39
40pub fn opt_string_field(key: &str, value: Option<&str>) -> String {
42 match value {
43 Some(v) => string_field(key, v),
44 None => format!("\"{}\":null", escape(key)),
45 }
46}
47
48pub fn string_array_field(key: &str, values: &[String]) -> String {
49 let items: Vec<String> = values
50 .iter()
51 .map(|v| format!("\"{}\"", escape(v)))
52 .collect();
53 format!("\"{}\":[{}]", escape(key), items.join(","))
54}
55
56pub fn object(fields: &[String]) -> String {
58 format!("{{{}}}", fields.join(","))
59}
60
61pub fn array(items: &[String]) -> String {
63 format!("[{}]", items.join(","))
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn escapes_quote_backslash_and_control_chars() {
72 assert_eq!(escape("a\"b"), "a\\\"b");
73 assert_eq!(escape("a\\b"), "a\\\\b");
74 assert_eq!(escape("a\nb"), "a\\nb");
75 assert_eq!(escape("a\rb"), "a\\rb");
76 assert_eq!(escape("a\tb"), "a\\tb");
77 assert_eq!(escape("a\u{1}b"), "a\\u0001b");
78 }
79
80 #[test]
81 fn plain_text_is_untouched() {
82 assert_eq!(escape("pre-commit-clippy"), "pre-commit-clippy");
83 assert_eq!(escape(""), "");
84 }
85
86 #[test]
87 fn fields_quote_both_key_and_string_value() {
88 assert_eq!(
89 string_field("id", "pre-commit-clippy"),
90 "\"id\":\"pre-commit-clippy\""
91 );
92 assert_eq!(bool_field("pushed", true), "\"pushed\":true");
93 assert_eq!(bool_field("pushed", false), "\"pushed\":false");
94 assert_eq!(opt_string_field("command", None), "\"command\":null");
95 assert_eq!(
96 opt_string_field("command", Some("ruff check")),
97 "\"command\":\"ruff check\""
98 );
99 }
100
101 #[test]
102 fn string_array_field_joins_and_escapes_each_element() {
103 assert_eq!(string_array_field("scope_files", &[]), "\"scope_files\":[]");
104 assert_eq!(
105 string_array_field("scope_files", &[".rs".to_string(), "a\"b".to_string()]),
106 "\"scope_files\":[\".rs\",\"a\\\"b\"]"
107 );
108 }
109
110 #[test]
111 fn object_and_array_join_with_commas() {
112 assert_eq!(
113 object(&["\"a\":1".into(), "\"b\":2".into()]),
114 "{\"a\":1,\"b\":2}"
115 );
116 assert_eq!(array(&["1".into(), "2".into()]), "[1,2]");
117 assert_eq!(object(&[]), "{}");
118 assert_eq!(array(&[]), "[]");
119 }
120}