json_builder_macro/
lib.rs

1pub 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        // TODO escape
39        self.buf.push_str(key);
40        self.buf.push_str("\":");
41        ToJSON::append_as_json_string(&value, self.buf);
42        self.started = true;
43    }
44
45    pub fn end(self) {
46        self.buf.push('}');
47    }
48}
49
50// TODO depth
51pub trait ToJSON {
52    fn as_json_string(&self) -> String {
53        let mut buf = String::new();
54        ToJSON::append_as_json_string(self, &mut buf);
55        buf
56    }
57
58    fn append_as_json_string(&self, buf: &mut String);
59}
60
61impl ToJSON for &str {
62    fn append_as_json_string(&self, buf: &mut String) {
63        buf.push('"');
64        buf.push_str(&escape_json_string(self));
65        buf.push('"')
66    }
67}
68
69impl ToJSON for String {
70    fn append_as_json_string(&self, buf: &mut String) {
71        ToJSON::append_as_json_string(&self.as_str(), buf)
72    }
73}
74
75impl<T: ToJSON> ToJSON for &[T] {
76    fn append_as_json_string(&self, buf: &mut String) {
77        buf.push('[');
78        for (idx, item) in self.iter().enumerate() {
79            if idx > 0 {
80                buf.push(',')
81            }
82            ToJSON::append_as_json_string(item, buf)
83        }
84        buf.push(']');
85    }
86}
87
88impl<T: ToJSON> ToJSON for Vec<T> {
89    fn append_as_json_string(&self, buf: &mut String) {
90        ToJSON::append_as_json_string(&self.as_slice(), buf)
91    }
92}
93
94impl<T1: ToJSON, T2: ToJSON> ToJSON for (T1, T2) {
95    fn append_as_json_string(&self, buf: &mut String) {
96        buf.push('[');
97        ToJSON::append_as_json_string(&self.0, buf);
98        buf.push(',');
99        ToJSON::append_as_json_string(&self.1, buf);
100        buf.push(']');
101    }
102}
103
104impl<T1: ToJSON, T2: ToJSON, T3: ToJSON> ToJSON for (T1, T2, T3) {
105    fn append_as_json_string(&self, buf: &mut String) {
106        buf.push('[');
107        ToJSON::append_as_json_string(&self.0, buf);
108        buf.push(',');
109        ToJSON::append_as_json_string(&self.1, buf);
110        buf.push(',');
111        ToJSON::append_as_json_string(&self.2, buf);
112        buf.push(']');
113    }
114}
115
116impl<K: AsRef<str>, V: ToJSON> ToJSON for std::collections::HashMap<K, V> {
117    fn append_as_json_string(&self, buf: &mut String) {
118        buf.push('{');
119        for (idx, (key, value)) in self.iter().enumerate() {
120            if idx > 0 {
121                buf.push(',')
122            }
123            buf.push('"');
124            buf.push_str(&escape_json_string(key.as_ref()));
125            buf.push_str("\":");
126            ToJSON::append_as_json_string(value, buf);
127        }
128        buf.push('}');
129    }
130}
131
132impl ToJSON for bool {
133    fn append_as_json_string(&self, buf: &mut String) {
134        buf.push_str(match self {
135            true => "true",
136            false => "false",
137        })
138    }
139}
140
141macro_rules! create_json_from_to_string_implementation {
142    ($($T:ty),*) => {
143        $(
144            impl ToJSON for $T {
145                fn append_as_json_string(&self, buf: &mut String) {
146                    buf.push_str(&self.to_string())
147                }
148            }
149        )*
150    }
151}
152
153// For all number types
154create_json_from_to_string_implementation![u8, u16, u32, u64, i8, i16, i32, i64, f32, f64];
155
156impl ToJSON for JSON<'_> {
157    fn append_as_json_string(&self, buf: &mut String) {
158        self.0.append_as_json_string(buf)
159    }
160}
161
162impl ToJSON for &'_ JSON<'_> {
163    fn append_as_json_string(&self, buf: &mut String) {
164        self.0.append_as_json_string(buf)
165    }
166}
167
168pub fn escape_json_string(on: &str) -> std::borrow::Cow<'_, str> {
169    let mut result = std::borrow::Cow::Borrowed("");
170    let mut start = 0;
171    for (index, matched) in on.match_indices(['\"', '\n', '\t', '\\']) {
172        result += &on[start..index];
173        result += "\\";
174        // I think this is correct?
175        result += match matched {
176            "\"" => "\"",
177            "\\" => "\\",
178            "\n" => "n",
179            "\t" => "t",
180            _ => unreachable!(),
181        };
182        start = index + 1;
183    }
184    result += &on[start..];
185    result
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn basic_object() {
194        let object = json! {
195            x: 78u32,
196            y: 72.4f64,
197            z: "thing"
198        };
199        assert_eq!(object, r#"{"x":78,"y":72.4,"z":"thing"}"#);
200    }
201
202    #[test]
203    fn escaping() {
204        let object = json! {
205            x: 78u32,
206            y: 72.4f64,
207            z: "thing\nover two lines"
208        };
209        assert_eq!(
210            object,
211            "{\"x\":78,\"y\":72.4,\"z\":\"thing\\nover two lines\"}"
212        );
213    }
214
215    #[test]
216    fn vec() {
217        let z = vec!["thing", "here"];
218        let object = json! {
219            x: 78u32,
220            y: 72.4f64,
221            z: z
222        };
223        assert_eq!(object, r#"{"x":78,"y":72.4,"z":["thing","here"]}"#);
224    }
225
226    #[test]
227    fn hash_map() {
228        let values = std::collections::HashMap::from_iter([("k1", "v1"), ("k2", "v2")]);
229        let object = json! {
230            kind: "map",
231            values: values
232        };
233        // because HashMap order is randomised, we test either cases
234        let possibles = [
235            r#"{"kind":"map","values":{"k1":"v1","k2":"v2"}}"#,
236            r#"{"kind":"map","values":{"k2":"v2","k1":"v1"}}"#,
237        ];
238        assert!(possibles.contains(&object.as_str()));
239    }
240}