json_builder_macro/
lib.rs

1#![doc = include_str!("README.md")]
2
3/// Wraps a JSON structure
4pub struct JSON<'a>(pub &'a dyn ToJSON);
5
6/// Takes input similar to JavaScript object-notation and generates string of JSON.
7///
8/// ```rust
9/// let object = json_builder_macro::json! {
10///     x: 78u32,
11///     y: 72.4f64,
12///     z: "thing"
13/// };
14/// assert_eq!(object, r#"{"x":78,"y":72.4,"z":"thing"}"#);
15/// ```
16#[macro_export]
17macro_rules! json {
18    {$( $key:ident : $val:expr ),* $(,)?} => {{
19		let mut buf = String::new();
20		let mut builder = $crate::Builder::new(&mut buf);
21		let pairs: &[(&str, $crate::JSON)] = &[$(
22			(stringify!($key), $crate::JSON(&$val)),
23		)*];
24		for (key, value) in pairs {
25			builder.add(key, value);
26		}
27		builder.end();
28		buf
29	}};
30}
31
32/// For *building up a JSON* object
33pub struct Builder<'a> {
34    started: bool,
35    buf: &'a mut String,
36}
37
38impl<'a> Builder<'a> {
39    pub fn new(buf: &'a mut String) -> Self {
40        buf.push('{');
41        Self {
42            started: false,
43            buf,
44        }
45    }
46
47    pub fn add(&mut self, key: &str, value: impl ToJSON) {
48        if self.started {
49            self.buf.push(',');
50        }
51        self.buf.push('"');
52        // TODO escape
53        self.buf.push_str(key);
54        self.buf.push_str("\":");
55        ToJSON::append_as_json_string(&value, self.buf);
56        self.started = true;
57    }
58
59    pub fn end(self) {
60        self.buf.push('}');
61    }
62}
63
64/// Represents a JSON object that can be serialized to JSON
65///
66/// TODO depth for pretty
67pub trait ToJSON {
68    fn as_json_string(&self) -> String {
69        let mut buf = String::new();
70        ToJSON::append_as_json_string(self, &mut buf);
71        buf
72    }
73
74    fn append_as_json_string(&self, buf: &mut String);
75}
76
77impl ToJSON for &str {
78    fn append_as_json_string(&self, buf: &mut String) {
79        buf.push('"');
80        buf.push_str(&escape_json_string(self));
81        buf.push('"')
82    }
83}
84
85impl ToJSON for String {
86    fn append_as_json_string(&self, buf: &mut String) {
87        ToJSON::append_as_json_string(&self.as_str(), buf)
88    }
89}
90
91impl<T: ToJSON> ToJSON for &[T] {
92    fn append_as_json_string(&self, buf: &mut String) {
93        buf.push('[');
94        for (idx, item) in self.iter().enumerate() {
95            if idx > 0 {
96                buf.push(',')
97            }
98            ToJSON::append_as_json_string(item, buf)
99        }
100        buf.push(']');
101    }
102}
103
104impl<T: ToJSON> ToJSON for Vec<T> {
105    fn append_as_json_string(&self, buf: &mut String) {
106        ToJSON::append_as_json_string(&self.as_slice(), buf)
107    }
108}
109
110impl<T1: ToJSON, T2: ToJSON> ToJSON for (T1, T2) {
111    fn append_as_json_string(&self, buf: &mut String) {
112        buf.push('[');
113        ToJSON::append_as_json_string(&self.0, buf);
114        buf.push(',');
115        ToJSON::append_as_json_string(&self.1, buf);
116        buf.push(']');
117    }
118}
119
120impl<T1: ToJSON, T2: ToJSON, T3: ToJSON> ToJSON for (T1, T2, T3) {
121    fn append_as_json_string(&self, buf: &mut String) {
122        buf.push('[');
123        ToJSON::append_as_json_string(&self.0, buf);
124        buf.push(',');
125        ToJSON::append_as_json_string(&self.1, buf);
126        buf.push(',');
127        ToJSON::append_as_json_string(&self.2, buf);
128        buf.push(']');
129    }
130}
131
132impl<K: AsRef<str>, V: ToJSON> ToJSON for std::collections::HashMap<K, V> {
133    fn append_as_json_string(&self, buf: &mut String) {
134        buf.push('{');
135        for (idx, (key, value)) in self.iter().enumerate() {
136            if idx > 0 {
137                buf.push(',')
138            }
139            buf.push('"');
140            buf.push_str(&escape_json_string(key.as_ref()));
141            buf.push_str("\":");
142            ToJSON::append_as_json_string(value, buf);
143        }
144        buf.push('}');
145    }
146}
147
148impl ToJSON for bool {
149    fn append_as_json_string(&self, buf: &mut String) {
150        buf.push_str(match self {
151            true => "true",
152            false => "false",
153        })
154    }
155}
156
157macro_rules! create_json_from_to_string_implementation {
158    ($($T:ty),*) => {
159        $(
160            impl ToJSON for $T {
161                fn append_as_json_string(&self, buf: &mut String) {
162                    buf.push_str(&self.to_string())
163                }
164            }
165        )*
166    }
167}
168
169// For all number types
170create_json_from_to_string_implementation![u8, u16, u32, u64, i8, i16, i32, i64, f32, f64];
171
172impl ToJSON for JSON<'_> {
173    fn append_as_json_string(&self, buf: &mut String) {
174        self.0.append_as_json_string(buf)
175    }
176}
177
178impl ToJSON for &'_ JSON<'_> {
179    fn append_as_json_string(&self, buf: &mut String) {
180        self.0.append_as_json_string(buf)
181    }
182}
183
184/// Escapes string content to be valid JSON
185pub fn escape_json_string(on: &str) -> std::borrow::Cow<'_, str> {
186    let mut result = std::borrow::Cow::Borrowed("");
187    let mut start = 0;
188    for (index, matched) in on.match_indices(['\"', '\n', '\t', '\\']) {
189        result += &on[start..index];
190        result += "\\";
191        // I think this is correct?
192        result += match matched {
193            "\"" => "\"",
194            "\\" => "\\",
195            "\n" => "n",
196            "\t" => "t",
197            _ => unreachable!(),
198        };
199        start = index + 1;
200    }
201    result += &on[start..];
202    result
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn basic_object() {
211        let object = json! {
212            x: 78u32,
213            y: 72.4f64,
214            z: "thing"
215        };
216        assert_eq!(object, r#"{"x":78,"y":72.4,"z":"thing"}"#);
217    }
218
219    #[test]
220    fn escaping() {
221        let object = json! {
222            x: 78u32,
223            y: 72.4f64,
224            z: "thing\nover two lines"
225        };
226        assert_eq!(
227            object,
228            "{\"x\":78,\"y\":72.4,\"z\":\"thing\\nover two lines\"}"
229        );
230    }
231
232    #[test]
233    fn vec() {
234        let z = vec!["thing", "here"];
235        let object = json! {
236            x: 78u32,
237            y: 72.4f64,
238            z: z
239        };
240        assert_eq!(object, r#"{"x":78,"y":72.4,"z":["thing","here"]}"#);
241    }
242
243    #[test]
244    fn hash_map() {
245        let values = std::collections::HashMap::from_iter([("k1", "v1"), ("k2", "v2")]);
246        let object = json! {
247            kind: "map",
248            values: values
249        };
250        // because HashMap order is randomised, we test either cases
251        let possibles = [
252            r#"{"kind":"map","values":{"k1":"v1","k2":"v2"}}"#,
253            r#"{"kind":"map","values":{"k2":"v2","k1":"v1"}}"#,
254        ];
255        assert!(possibles.contains(&object.as_str()));
256    }
257}