aws_smithy_json/
serialize.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::escape::escape_string;
7use aws_smithy_types::date_time::{DateTimeFormatError, Format};
8use aws_smithy_types::primitive::Encoder;
9use aws_smithy_types::{DateTime, Document, Number};
10use std::borrow::Cow;
11
12pub struct JsonValueWriter<'a> {
13    output: &'a mut String,
14}
15
16impl<'a> JsonValueWriter<'a> {
17    pub fn new(output: &'a mut String) -> Self {
18        JsonValueWriter { output }
19    }
20
21    /// Writes a null value.
22    pub fn null(self) {
23        self.output.push_str("null");
24    }
25
26    /// Writes the boolean `value`.
27    pub fn boolean(self, value: bool) {
28        self.output.push_str(match value {
29            true => "true",
30            _ => "false",
31        });
32    }
33
34    /// Writes a document `value`.
35    pub fn document(self, value: &Document) {
36        match value {
37            Document::Array(values) => {
38                let mut array = self.start_array();
39                for value in values {
40                    array.value().document(value);
41                }
42                array.finish();
43            }
44            Document::Bool(value) => self.boolean(*value),
45            Document::Null => self.null(),
46            Document::Number(value) => self.number(*value),
47            Document::Object(values) => {
48                let mut object = self.start_object();
49                for (key, value) in values {
50                    object.key(key).document(value);
51                }
52                object.finish();
53            }
54            Document::String(value) => self.string(value),
55        }
56    }
57
58    /// Writes a string `value`.
59    pub fn string(self, value: &str) {
60        self.output.push('"');
61        self.output.push_str(&escape_string(value));
62        self.output.push('"');
63    }
64
65    /// Writes a string `value` without escaping it.
66    pub fn string_unchecked(self, value: &str) {
67        // Verify in debug builds that we don't actually need to escape the string
68        debug_assert!(matches!(escape_string(value), Cow::Borrowed(_)));
69
70        self.output.push('"');
71        self.output.push_str(value);
72        self.output.push('"');
73    }
74
75    /// Writes a number `value`.
76    pub fn number(self, value: Number) {
77        match value {
78            Number::PosInt(value) => {
79                // itoa::Buffer is a fixed-size stack allocation, so this is cheap
80                self.output.push_str(Encoder::from(value).encode());
81            }
82            Number::NegInt(value) => {
83                self.output.push_str(Encoder::from(value).encode());
84            }
85            Number::Float(value) => {
86                let mut encoder: Encoder = value.into();
87                // Nan / infinite values actually get written in quotes as a string value
88                if value.is_infinite() || value.is_nan() {
89                    self.string_unchecked(encoder.encode())
90                } else {
91                    self.output.push_str(encoder.encode())
92                }
93            }
94        }
95    }
96
97    /// Writes a date-time `value` with the given `format`.
98    pub fn date_time(
99        self,
100        date_time: &DateTime,
101        format: Format,
102    ) -> Result<(), DateTimeFormatError> {
103        let formatted = date_time.fmt(format)?;
104        match format {
105            Format::EpochSeconds => self.output.push_str(&formatted),
106            _ => self.string(&formatted),
107        }
108        Ok(())
109    }
110
111    /// Starts an array.
112    pub fn start_array(self) -> JsonArrayWriter<'a> {
113        JsonArrayWriter::new(self.output)
114    }
115
116    /// Starts an object.
117    pub fn start_object(self) -> JsonObjectWriter<'a> {
118        JsonObjectWriter::new(self.output)
119    }
120}
121
122pub struct JsonObjectWriter<'a> {
123    json: &'a mut String,
124    started: bool,
125}
126
127impl<'a> JsonObjectWriter<'a> {
128    pub fn new(output: &'a mut String) -> Self {
129        output.push('{');
130        Self {
131            json: output,
132            started: false,
133        }
134    }
135
136    /// Starts a value with the given `key`.
137    pub fn key(&mut self, key: &str) -> JsonValueWriter<'_> {
138        if self.started {
139            self.json.push(',');
140        }
141        self.started = true;
142
143        self.json.push('"');
144        self.json.push_str(&escape_string(key));
145        self.json.push_str("\":");
146
147        JsonValueWriter::new(self.json)
148    }
149
150    /// Finishes the object.
151    pub fn finish(self) {
152        self.json.push('}');
153    }
154}
155
156pub struct JsonArrayWriter<'a> {
157    json: &'a mut String,
158    started: bool,
159}
160
161impl<'a> JsonArrayWriter<'a> {
162    pub fn new(output: &'a mut String) -> Self {
163        output.push('[');
164        Self {
165            json: output,
166            started: false,
167        }
168    }
169
170    /// Starts a new value in the array.
171    pub fn value(&mut self) -> JsonValueWriter<'_> {
172        self.comma_delimit();
173        JsonValueWriter::new(self.json)
174    }
175
176    /// Finishes the array.
177    pub fn finish(self) {
178        self.json.push(']');
179    }
180
181    fn comma_delimit(&mut self) {
182        if self.started {
183            self.json.push(',');
184        }
185        self.started = true;
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::{JsonArrayWriter, JsonObjectWriter};
192    use crate::serialize::JsonValueWriter;
193    use aws_smithy_types::date_time::Format;
194    use aws_smithy_types::{DateTime, Document, Number};
195    use proptest::proptest;
196
197    #[test]
198    fn empty() {
199        let mut output = String::new();
200        JsonObjectWriter::new(&mut output).finish();
201        assert_eq!("{}", &output);
202
203        let mut output = String::new();
204        JsonArrayWriter::new(&mut output).finish();
205        assert_eq!("[]", &output);
206    }
207
208    #[test]
209    fn object_inside_array() {
210        let mut output = String::new();
211        let mut array = JsonArrayWriter::new(&mut output);
212        array.value().start_object().finish();
213        array.value().start_object().finish();
214        array.value().start_object().finish();
215        array.finish();
216        assert_eq!("[{},{},{}]", &output);
217    }
218
219    #[test]
220    fn object_inside_object() {
221        let mut output = String::new();
222        let mut obj_1 = JsonObjectWriter::new(&mut output);
223
224        let mut obj_2 = obj_1.key("nested").start_object();
225        obj_2.key("test").string("test");
226        obj_2.finish();
227
228        obj_1.finish();
229        assert_eq!(r#"{"nested":{"test":"test"}}"#, &output);
230    }
231
232    #[test]
233    fn array_inside_object() {
234        let mut output = String::new();
235        let mut object = JsonObjectWriter::new(&mut output);
236        object.key("foo").start_array().finish();
237        object.key("ba\nr").start_array().finish();
238        object.finish();
239        assert_eq!(r#"{"foo":[],"ba\nr":[]}"#, &output);
240    }
241
242    #[test]
243    fn array_inside_array() {
244        let mut output = String::new();
245
246        let mut arr_1 = JsonArrayWriter::new(&mut output);
247
248        let mut arr_2 = arr_1.value().start_array();
249        arr_2.value().number(Number::PosInt(5));
250        arr_2.finish();
251
252        arr_1.value().start_array().finish();
253        arr_1.finish();
254
255        assert_eq!("[[5],[]]", &output);
256    }
257
258    #[test]
259    fn object() {
260        let mut output = String::new();
261        let mut object = JsonObjectWriter::new(&mut output);
262        object.key("true_val").boolean(true);
263        object.key("false_val").boolean(false);
264        object.key("some_string").string("some\nstring\nvalue");
265        object.key("unchecked_str").string_unchecked("unchecked");
266        object.key("some_number").number(Number::Float(3.5));
267        object.key("some_null").null();
268
269        let mut array = object.key("some_mixed_array").start_array();
270        array.value().string("1");
271        array.value().number(Number::NegInt(-2));
272        array.value().string_unchecked("unchecked");
273        array.value().boolean(true);
274        array.value().boolean(false);
275        array.value().null();
276        array.finish();
277
278        object.finish();
279
280        assert_eq!(
281            r#"{"true_val":true,"false_val":false,"some_string":"some\nstring\nvalue","unchecked_str":"unchecked","some_number":3.5,"some_null":null,"some_mixed_array":["1",-2,"unchecked",true,false,null]}"#,
282            &output
283        );
284    }
285
286    #[test]
287    fn object_date_times() {
288        let mut output = String::new();
289
290        let mut object = JsonObjectWriter::new(&mut output);
291        object
292            .key("epoch_seconds")
293            .date_time(&DateTime::from_secs_f64(5.2), Format::EpochSeconds)
294            .unwrap();
295        object
296            .key("date_time")
297            .date_time(
298                &DateTime::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(),
299                Format::DateTime,
300            )
301            .unwrap();
302        object
303            .key("http_date")
304            .date_time(
305                &DateTime::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(),
306                Format::HttpDate,
307            )
308            .unwrap();
309        object.finish();
310
311        assert_eq!(
312            r#"{"epoch_seconds":5.2,"date_time":"2021-05-24T15:34:50.123Z","http_date":"Wed, 21 Oct 2015 07:28:00 GMT"}"#,
313            &output,
314        )
315    }
316
317    #[test]
318    fn array_date_times() {
319        let mut output = String::new();
320
321        let mut array = JsonArrayWriter::new(&mut output);
322        array
323            .value()
324            .date_time(&DateTime::from_secs_f64(5.2), Format::EpochSeconds)
325            .unwrap();
326        array
327            .value()
328            .date_time(
329                &DateTime::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(),
330                Format::DateTime,
331            )
332            .unwrap();
333        array
334            .value()
335            .date_time(
336                &DateTime::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(),
337                Format::HttpDate,
338            )
339            .unwrap();
340        array.finish();
341
342        assert_eq!(
343            r#"[5.2,"2021-05-24T15:34:50.123Z","Wed, 21 Oct 2015 07:28:00 GMT"]"#,
344            &output,
345        )
346    }
347
348    fn format_document(document: Document) -> String {
349        let mut output = String::new();
350        JsonValueWriter::new(&mut output).document(&document);
351        output
352    }
353
354    #[test]
355    fn document() {
356        assert_eq!("null", format_document(Document::Null));
357        assert_eq!("true", format_document(Document::Bool(true)));
358        assert_eq!("false", format_document(Document::Bool(false)));
359        assert_eq!("5", format_document(Document::Number(Number::PosInt(5))));
360        assert_eq!("\"test\"", format_document(Document::String("test".into())));
361        assert_eq!(
362            "[null,true,\"test\"]",
363            format_document(Document::Array(vec![
364                Document::Null,
365                Document::Bool(true),
366                Document::String("test".into())
367            ]))
368        );
369        assert_eq!(
370            r#"{"test":"foo"}"#,
371            format_document(Document::Object(
372                vec![("test".to_string(), Document::String("foo".into()))]
373                    .into_iter()
374                    .collect()
375            ))
376        );
377        assert_eq!(
378            r#"{"test1":[{"num":1},{"num":2}]}"#,
379            format_document(Document::Object(
380                vec![(
381                    "test1".to_string(),
382                    Document::Array(vec![
383                        Document::Object(
384                            vec![("num".to_string(), Document::Number(Number::PosInt(1))),]
385                                .into_iter()
386                                .collect()
387                        ),
388                        Document::Object(
389                            vec![("num".to_string(), Document::Number(Number::PosInt(2))),]
390                                .into_iter()
391                                .collect()
392                        ),
393                    ])
394                ),]
395                .into_iter()
396                .collect()
397            ))
398        );
399    }
400
401    fn format_test_number(number: Number) -> String {
402        let mut formatted = String::new();
403        JsonValueWriter::new(&mut formatted).number(number);
404        formatted
405    }
406
407    #[test]
408    fn number_formatting() {
409        assert_eq!("1", format_test_number(Number::PosInt(1)));
410        assert_eq!("-1", format_test_number(Number::NegInt(-1)));
411        assert_eq!("1", format_test_number(Number::NegInt(1)));
412        assert_eq!("0.0", format_test_number(Number::Float(0.0)));
413        assert_eq!("10000000000.0", format_test_number(Number::Float(1e10)));
414        assert_eq!("-1.2", format_test_number(Number::Float(-1.2)));
415
416        // Smithy has specific behavior for infinity & NaN
417        // the behavior of the serde_json crate in these cases.
418        assert_eq!("\"NaN\"", format_test_number(Number::Float(f64::NAN)));
419        assert_eq!(
420            "\"Infinity\"",
421            format_test_number(Number::Float(f64::INFINITY))
422        );
423        assert_eq!(
424            "\"-Infinity\"",
425            format_test_number(Number::Float(f64::NEG_INFINITY))
426        );
427    }
428
429    proptest! {
430        #[test]
431        fn matches_serde_json_pos_int_format(value: u64) {
432            assert_eq!(
433                serde_json::to_string(&value).unwrap(),
434                format_test_number(Number::PosInt(value)),
435            )
436        }
437
438        #[test]
439        fn matches_serde_json_neg_int_format(value: i64) {
440            assert_eq!(
441                serde_json::to_string(&value).unwrap(),
442                format_test_number(Number::NegInt(value)),
443            )
444        }
445
446        #[test]
447        fn matches_serde_json_float_format(value: f64) {
448            assert_eq!(
449                serde_json::to_string(&value).unwrap(),
450                format_test_number(Number::Float(value)),
451            )
452        }
453    }
454}