json_builder_macro/
lib.rs1pub struct JSON<'a>(pub &'a dyn ToJSON);
2
3#[macro_export]
4macro_rules! json {
5 {$( $key:ident : $val:expr ),* $(,)?} => {{
6 let mut buf = String::new();
7 let mut builder = Builder::new(&mut buf);
8 let pairs: &[(&str, JSON)] = &[$(
9 (stringify!($key), JSON(&$val)),
10 )*];
11 for (key, value) in pairs {
12 builder.add(key, value);
13 }
14 builder.end();
15 buf
16 }};
17}
18
19pub struct Builder<'a> {
20 started: bool,
21 buf: &'a mut String,
22}
23
24impl<'a> Builder<'a> {
25 pub fn new(buf: &'a mut String) -> Self {
26 buf.push('{');
27 Self {
28 started: false,
29 buf,
30 }
31 }
32
33 pub fn add(&mut self, key: &str, value: impl ToJSON) {
34 if self.started {
35 self.buf.push(',');
36 }
37 self.buf.push('"');
38 self.buf.push_str(key);
40 self.buf.push_str("\":");
41 ToJSON::append(&value, self.buf);
42 self.started = true;
43 }
44
45 pub fn end(self) {
46 self.buf.push('}');
47 }
48}
49
50pub trait ToJSON {
52 fn append(&self, buf: &mut String);
53}
54
55impl ToJSON for &str {
56 fn append(&self, buf: &mut String) {
57 buf.push('"');
58 buf.push_str(&escape_json_string(self));
59 buf.push('"')
60 }
61}
62
63impl ToJSON for String {
64 fn append(&self, buf: &mut String) {
65 ToJSON::append(&self.as_str(), buf)
66 }
67}
68
69impl<T: ToJSON> ToJSON for &[T] {
70 fn append(&self, buf: &mut String) {
71 buf.push('[');
72 for (idx, item) in self.iter().enumerate() {
73 if idx > 0 {
74 buf.push(',')
75 }
76 ToJSON::append(item, buf)
77 }
78 buf.push(']');
79 }
80}
81
82impl<T: ToJSON> ToJSON for Vec<T> {
83 fn append(&self, buf: &mut String) {
84 ToJSON::append(&self.as_slice(), buf)
85 }
86}
87
88impl<K: AsRef<str>, V: ToJSON> ToJSON for std::collections::HashMap<K, V> {
89 fn append(&self, buf: &mut String) {
90 buf.push('{');
91 for (idx, (key, value)) in self.iter().enumerate() {
92 if idx > 0 {
93 buf.push(',')
94 }
95 buf.push('"');
96 buf.push_str(&escape_json_string(key.as_ref()));
97 buf.push_str("\":");
98 ToJSON::append(value, buf);
99 }
100 buf.push('}');
101 }
102}
103
104impl ToJSON for bool {
105 fn append(&self, buf: &mut String) {
106 buf.push_str(match self {
107 true => "true",
108 false => "false",
109 })
110 }
111}
112
113macro_rules! create_json_from_to_string_implementation {
114 ($($T:ty),*) => {
115 $(
116 impl ToJSON for $T {
117 fn append(&self, buf: &mut String) {
118 buf.push_str(&self.to_string())
119 }
120 }
121 )*
122 }
123}
124
125create_json_from_to_string_implementation![u8, u16, u32, u64, i8, i16, i32, i64, f32, f64];
127
128impl ToJSON for JSON<'_> {
129 fn append(&self, buf: &mut String) {
130 self.0.append(buf)
131 }
132}
133
134impl ToJSON for &'_ JSON<'_> {
135 fn append(&self, buf: &mut String) {
136 self.0.append(buf)
137 }
138}
139
140pub fn escape_json_string(on: &str) -> std::borrow::Cow<'_, str> {
141 let mut result = std::borrow::Cow::Borrowed("");
142 let mut start = 0;
143 for (index, matched) in on.match_indices(['\"', '\n', '\t', '\\']) {
144 result += &on[start..index];
145 result += "\\";
146 result += match matched {
148 "\"" => "\"",
149 "\\" => "\\",
150 "\n" => "n",
151 "\t" => "t",
152 _ => unreachable!(),
153 };
154 start = index + 1;
155 }
156 result += &on[start..];
157 result
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn basic_object() {
166 let object = json! {
167 x: 78u32,
168 y: 72.4f64,
169 z: "thing"
170 };
171 assert_eq!(object, r#"{"x":78,"y":72.4,"z":"thing"}"#);
172 }
173
174 #[test]
175 fn escaping() {
176 let object = json! {
177 x: 78u32,
178 y: 72.4f64,
179 z: "thing\nover two lines"
180 };
181 assert_eq!(
182 object,
183 "{\"x\":78,\"y\":72.4,\"z\":\"thing\\nover two lines\"}"
184 );
185 }
186
187 #[test]
188 fn vec() {
189 let z = vec!["thing", "here"];
190 let object = json! {
191 x: 78u32,
192 y: 72.4f64,
193 z: z
194 };
195 assert_eq!(object, r#"{"x":78,"y":72.4,"z":["thing","here"]}"#);
196 }
197
198 #[test]
199 fn hash_map() {
200 let values = std::collections::HashMap::from_iter([("k1", "v1"), ("k2", "v2")]);
201 let object = json! {
202 kind: "map",
203 values: values
204 };
205 let possibles = [
207 r#"{"kind":"map","values":{"k1":"v1","k2":"v2"}}"#,
208 r#"{"kind":"map","values":{"k2":"v2","k1":"v1"}}"#,
209 ];
210 assert!(possibles.contains(&object.as_str()));
211 }
212}