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