1#![doc = include_str!("README.md")]
2
3pub struct JSON<'a>(pub &'a dyn ToJSON);
5
6#[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
32pub 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 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
64pub 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
169create_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
184pub 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 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 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}