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(&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 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
125// For all number types
126create_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        // I think this is correct?
147        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        // because HashMap order is randomised, we test either cases
206        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}