1 + | /*
|
2 + | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
3 + | * SPDX-License-Identifier: Apache-2.0
|
4 + | */
|
5 + |
|
6 + | //! JSON serializer implementation.
|
7 + |
|
8 + | use aws_smithy_schema::serde::ShapeSerializer;
|
9 + | use aws_smithy_schema::{Schema, ShapeId};
|
10 + | use aws_smithy_types::date_time::Format as TimestampFormat;
|
11 + | use aws_smithy_types::{BigDecimal, BigInteger, Blob, DateTime, Document};
|
12 + | use std::fmt;
|
13 + |
|
14 + | use crate::codec::JsonCodecSettings;
|
15 + |
|
16 + | /// Error type for JSON serialization.
|
17 + | #[derive(Debug)]
|
18 + | pub enum JsonSerializerError {
|
19 + | /// An error occurred during JSON writing.
|
20 + | WriteError(String),
|
21 + | }
|
22 + |
|
23 + | impl fmt::Display for JsonSerializerError {
|
24 + | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
25 + | match self {
|
26 + | Self::WriteError(msg) => write!(f, "JSON write error: {}", msg),
|
27 + | }
|
28 + | }
|
29 + | }
|
30 + |
|
31 + | impl std::error::Error for JsonSerializerError {}
|
32 + |
|
33 + | /// JSON serializer that implements the ShapeSerializer trait.
|
34 + | pub struct JsonSerializer {
|
35 + | output: String,
|
36 + | settings: JsonCodecSettings,
|
37 + | }
|
38 + |
|
39 + | impl JsonSerializer {
|
40 + | /// Creates a new JSON serializer with the given settings.
|
41 + | pub fn new(settings: JsonCodecSettings) -> Self {
|
42 + | Self {
|
43 + | output: String::new(),
|
44 + | settings,
|
45 + | }
|
46 + | }
|
47 + |
|
48 + | /// Gets the timestamp format to use, respecting @timestampFormat trait.
|
49 + | fn get_timestamp_format(&self, schema: &dyn Schema) -> TimestampFormat {
|
50 + | let timestamp_format_trait_id = ShapeId::new("smithy.api#timestampFormat");
|
51 + | if let Some(trait_obj) = schema.traits().get(×tamp_format_trait_id) {
|
52 + | if let Some(format_str) = trait_obj.as_any().downcast_ref::<String>() {
|
53 + | return match format_str.as_str() {
|
54 + | "epoch-seconds" => TimestampFormat::EpochSeconds,
|
55 + | "http-date" => TimestampFormat::HttpDate,
|
56 + | "date-time" => TimestampFormat::DateTime,
|
57 + | _ => self.settings.default_timestamp_format,
|
58 + | };
|
59 + | }
|
60 + | }
|
61 + | self.settings.default_timestamp_format
|
62 + | }
|
63 + |
|
64 + | fn write_json_value(&mut self, doc: &Document) {
|
65 + | use crate::serialize::JsonValueWriter;
|
66 + | let writer = JsonValueWriter::new(&mut self.output);
|
67 + | writer.document(doc);
|
68 + | }
|
69 + | }
|
70 + |
|
71 + | impl ShapeSerializer for JsonSerializer {
|
72 + | type Output = Vec<u8>;
|
73 + | type Error = JsonSerializerError;
|
74 + |
|
75 + | fn finish(self) -> Result<Self::Output, Self::Error> {
|
76 + | Ok(self.output.into_bytes())
|
77 + | }
|
78 + |
|
79 + | fn write_struct<F>(&mut self, _schema: &dyn Schema, write_members: F) -> Result<(), Self::Error>
|
80 + | where
|
81 + | F: FnOnce(&mut Self) -> Result<(), Self::Error>,
|
82 + | {
|
83 + | self.output.push('{');
|
84 + | write_members(self)?;
|
85 + | self.output.push('}');
|
86 + | Ok(())
|
87 + | }
|
88 + |
|
89 + | fn write_list<F>(&mut self, _schema: &dyn Schema, write_elements: F) -> Result<(), Self::Error>
|
90 + | where
|
91 + | F: FnOnce(&mut Self) -> Result<(), Self::Error>,
|
92 + | {
|
93 + | self.output.push('[');
|
94 + | write_elements(self)?;
|
95 + | self.output.push(']');
|
96 + | Ok(())
|
97 + | }
|
98 + |
|
99 + | fn write_map<F>(&mut self, _schema: &dyn Schema, write_entries: F) -> Result<(), Self::Error>
|
100 + | where
|
101 + | F: FnOnce(&mut Self) -> Result<(), Self::Error>,
|
102 + | {
|
103 + | self.output.push('{');
|
104 + | write_entries(self)?;
|
105 + | self.output.push('}');
|
106 + | Ok(())
|
107 + | }
|
108 + |
|
109 + | fn write_boolean(&mut self, _schema: &dyn Schema, value: bool) -> Result<(), Self::Error> {
|
110 + | self.output.push_str(if value { "true" } else { "false" });
|
111 + | Ok(())
|
112 + | }
|
113 + |
|
114 + | fn write_byte(&mut self, _schema: &dyn Schema, value: i8) -> Result<(), Self::Error> {
|
115 + | use std::fmt::Write;
|
116 + | write!(&mut self.output, "{}", value)
|
117 + | .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
|
118 + | }
|
119 + |
|
120 + | fn write_short(&mut self, _schema: &dyn Schema, value: i16) -> Result<(), Self::Error> {
|
121 + | use std::fmt::Write;
|
122 + | write!(&mut self.output, "{}", value)
|
123 + | .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
|
124 + | }
|
125 + |
|
126 + | fn write_integer(&mut self, _schema: &dyn Schema, value: i32) -> Result<(), Self::Error> {
|
127 + | use std::fmt::Write;
|
128 + | write!(&mut self.output, "{}", value)
|
129 + | .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
|
130 + | }
|
131 + |
|
132 + | fn write_long(&mut self, _schema: &dyn Schema, value: i64) -> Result<(), Self::Error> {
|
133 + | use std::fmt::Write;
|
134 + | write!(&mut self.output, "{}", value)
|
135 + | .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
|
136 + | }
|
137 + |
|
138 + | fn write_float(&mut self, _schema: &dyn Schema, value: f32) -> Result<(), Self::Error> {
|
139 + | use std::fmt::Write;
|
140 + | write!(&mut self.output, "{}", value)
|
141 + | .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
|
142 + | }
|
143 + |
|
144 + | fn write_double(&mut self, _schema: &dyn Schema, value: f64) -> Result<(), Self::Error> {
|
145 + | use std::fmt::Write;
|
146 + | write!(&mut self.output, "{}", value)
|
147 + | .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
|
148 + | }
|
149 + |
|
150 + | fn write_big_integer(
|
151 + | &mut self,
|
152 + | _schema: &dyn Schema,
|
153 + | value: &BigInteger,
|
154 + | ) -> Result<(), Self::Error> {
|
155 + | self.output.push_str(value.as_ref());
|
156 + | Ok(())
|
157 + | }
|
158 + |
|
159 + | fn write_big_decimal(
|
160 + | &mut self,
|
161 + | _schema: &dyn Schema,
|
162 + | value: &BigDecimal,
|
163 + | ) -> Result<(), Self::Error> {
|
164 + | self.output.push_str(value.as_ref());
|
165 + | Ok(())
|
166 + | }
|
167 + |
|
168 + | fn write_string(&mut self, _schema: &dyn Schema, value: &str) -> Result<(), Self::Error> {
|
169 + | use crate::escape::escape_string;
|
170 + | self.output.push('"');
|
171 + | self.output.push_str(&escape_string(value));
|
172 + | self.output.push('"');
|
173 + | Ok(())
|
174 + | }
|
175 + |
|
176 + | fn write_blob(&mut self, _schema: &dyn Schema, value: &Blob) -> Result<(), Self::Error> {
|
177 + | use aws_smithy_types::base64;
|
178 + | let encoded = base64::encode(value.as_ref());
|
179 + | self.output.push('"');
|
180 + | self.output.push_str(&encoded);
|
181 + | self.output.push('"');
|
182 + | Ok(())
|
183 + | }
|
184 + |
|
185 + | fn write_timestamp(
|
186 + | &mut self,
|
187 + | schema: &dyn Schema,
|
188 + | value: &DateTime,
|
189 + | ) -> Result<(), Self::Error> {
|
190 + | let format = self.get_timestamp_format(schema);
|
191 + | let formatted = value.fmt(format).map_err(|e| {
|
192 + | JsonSerializerError::WriteError(format!("Failed to format timestamp: {}", e))
|
193 + | })?;
|
194 + |
|
195 + | match format {
|
196 + | TimestampFormat::EpochSeconds => {
|
197 + | // Epoch seconds as number
|
198 + | self.output.push_str(&formatted);
|
199 + | }
|
200 + | _ => {
|
201 + | // Other formats as strings
|
202 + | self.output.push('"');
|
203 + | self.output.push_str(&formatted);
|
204 + | self.output.push('"');
|
205 + | }
|
206 + | }
|
207 + | Ok(())
|
208 + | }
|
209 + |
|
210 + | fn write_document(
|
211 + | &mut self,
|
212 + | _schema: &dyn Schema,
|
213 + | value: &Document,
|
214 + | ) -> Result<(), Self::Error> {
|
215 + | self.write_json_value(value);
|
216 + | Ok(())
|
217 + | }
|
218 + |
|
219 + | fn write_null(&mut self, _schema: &dyn Schema) -> Result<(), Self::Error> {
|
220 + | self.output.push_str("null");
|
221 + | Ok(())
|
222 + | }
|
223 + | }
|
224 + |
|
225 + | #[cfg(test)]
|
226 + | mod tests {
|
227 + | use super::*;
|
228 + | use aws_smithy_schema::prelude::*;
|
229 + |
|
230 + | #[test]
|
231 + | fn test_write_boolean() {
|
232 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
233 + | ser.write_boolean(&BOOLEAN, true).unwrap();
|
234 + | let output = ser.finish().unwrap();
|
235 + | assert_eq!(String::from_utf8(output).unwrap(), "true");
|
236 + | }
|
237 + |
|
238 + | #[test]
|
239 + | fn test_write_string() {
|
240 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
241 + | ser.write_string(&STRING, "hello").unwrap();
|
242 + | let output = ser.finish().unwrap();
|
243 + | assert_eq!(String::from_utf8(output).unwrap(), "\"hello\"");
|
244 + | }
|
245 + |
|
246 + | #[test]
|
247 + | fn test_write_integer() {
|
248 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
249 + | ser.write_integer(&INTEGER, 42).unwrap();
|
250 + | let output = ser.finish().unwrap();
|
251 + | assert_eq!(String::from_utf8(output).unwrap(), "42");
|
252 + | }
|
253 + |
|
254 + | #[test]
|
255 + | fn test_write_null() {
|
256 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
257 + | ser.write_null(&STRING).unwrap();
|
258 + | let output = ser.finish().unwrap();
|
259 + | assert_eq!(String::from_utf8(output).unwrap(), "null");
|
260 + | }
|
261 + |
|
262 + | #[test]
|
263 + | fn test_write_list() {
|
264 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
265 + | // Create a simple list schema for testing
|
266 + | let list_schema = aws_smithy_schema::prelude::PreludeSchema::new(
|
267 + | aws_smithy_schema::ShapeId::new("test#List"),
|
268 + | aws_smithy_schema::ShapeType::List,
|
269 + | );
|
270 + | ser.write_list(&list_schema, |s| {
|
271 + | s.write_integer(&INTEGER, 1)?;
|
272 + | s.output.push(',');
|
273 + | s.write_integer(&INTEGER, 2)?;
|
274 + | s.output.push(',');
|
275 + | s.write_integer(&INTEGER, 3)?;
|
276 + | Ok(())
|
277 + | })
|
278 + | .unwrap();
|
279 + | let output = ser.finish().unwrap();
|
280 + | assert_eq!(String::from_utf8(output).unwrap(), "[1,2,3]");
|
281 + | }
|
282 + |
|
283 + | #[test]
|
284 + | fn test_write_full_object() {
|
285 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
286 + | let struct_schema = aws_smithy_schema::prelude::PreludeSchema::new(
|
287 + | aws_smithy_schema::ShapeId::new("test#Struct"),
|
288 + | aws_smithy_schema::ShapeType::Structure,
|
289 + | );
|
290 + | let list_schema = aws_smithy_schema::prelude::PreludeSchema::new(
|
291 + | aws_smithy_schema::ShapeId::new("test#List"),
|
292 + | aws_smithy_schema::ShapeType::List,
|
293 + | );
|
294 + | ser.write_struct(&struct_schema, |s| {
|
295 + | s.output.push_str("\"active\":");
|
296 + | s.write_boolean(&BOOLEAN, true)?;
|
297 + | s.output.push(',');
|
298 + | s.output.push_str("\"name\":");
|
299 + | s.write_string(&STRING, "test")?;
|
300 + | s.output.push(',');
|
301 + | s.output.push_str("\"count\":");
|
302 + | s.write_integer(&INTEGER, 42)?;
|
303 + | s.output.push(',');
|
304 + | s.output.push_str("\"price\":");
|
305 + | s.write_float(&FLOAT, 3.14)?;
|
306 + | s.output.push(',');
|
307 + | s.output.push_str("\"items\":");
|
308 + | s.write_list(&list_schema, |ls| {
|
309 + | ls.write_integer(&INTEGER, 1)?;
|
310 + | ls.output.push(',');
|
311 + | ls.write_integer(&INTEGER, 2)?;
|
312 + | Ok(())
|
313 + | })?;
|
314 + | Ok(())
|
315 + | })
|
316 + | .unwrap();
|
317 + | let output = ser.finish().unwrap();
|
318 + | assert_eq!(
|
319 + | String::from_utf8(output).unwrap(),
|
320 + | "{\"active\":true,\"name\":\"test\",\"count\":42,\"price\":3.14,\"items\":[1,2]}"
|
321 + | );
|
322 + | }
|
323 + |
|
324 + | #[test]
|
325 + | fn test_nested_complex_serialization() {
|
326 + | let mut ser = JsonSerializer::new(JsonCodecSettings::default());
|
327 + | let struct_schema = aws_smithy_schema::prelude::PreludeSchema::new(
|
328 + | aws_smithy_schema::ShapeId::new("test#User"),
|
329 + | aws_smithy_schema::ShapeType::Structure,
|
330 + | );
|
331 + | let list_schema = aws_smithy_schema::prelude::PreludeSchema::new(
|
332 + | aws_smithy_schema::ShapeId::new("test#List"),
|
333 + | aws_smithy_schema::ShapeType::List,
|
334 + | );
|
335 + | let map_schema = aws_smithy_schema::prelude::PreludeSchema::new(
|
336 + | aws_smithy_schema::ShapeId::new("test#Map"),
|
337 + | aws_smithy_schema::ShapeType::Map,
|
338 + | );
|
339 + |
|
340 + | ser.write_struct(&struct_schema, |s| {
|
341 + | s.output.push_str("\"id\":");
|
342 + | s.write_long(&LONG, 12345)?;
|
343 + | s.output.push(',');
|
344 + | s.output.push_str("\"name\":");
|
345 + | s.write_string(&STRING, "John Doe")?;
|
346 + | s.output.push(',');
|
347 + | s.output.push_str("\"scores\":");
|
348 + | s.write_list(&list_schema, |ls| {
|
349 + | ls.write_double(&DOUBLE, 95.5)?;
|
350 + | ls.output.push(',');
|
351 + | ls.write_double(&DOUBLE, 87.3)?;
|
352 + | ls.output.push(',');
|
353 + | ls.write_double(&DOUBLE, 92.1)?;
|
354 + | Ok(())
|
355 + | })?;
|
356 + | s.output.push(',');
|
357 + | s.output.push_str("\"address\":");
|
358 + | s.write_struct(&struct_schema, |addr| {
|
359 + | addr.output.push_str("\"street\":");
|
360 + | addr.write_string(&STRING, "123 Main St")?;
|
361 + | addr.output.push(',');
|
362 + | addr.output.push_str("\"city\":");
|
363 + | addr.write_string(&STRING, "Seattle")?;
|
364 + | addr.output.push(',');
|
365 + | addr.output.push_str("\"zip\":");
|
366 + | addr.write_integer(&INTEGER, 98101)?;
|
367 + | Ok(())
|
368 + | })?;
|
369 + | s.output.push(',');
|
370 + | s.output.push_str("\"companies\":");
|
371 + | s.write_list(&list_schema, |ls| {
|
372 + | ls.write_struct(&struct_schema, |comp| {
|
373 + | comp.output.push_str("\"name\":");
|
374 + | comp.write_string(&STRING, "TechCorp")?;
|
375 + | comp.output.push(',');
|
376 + | comp.output.push_str("\"employees\":");
|
377 + | comp.write_list(&list_schema, |emp| {
|
378 + | emp.write_string(&STRING, "Alice")?;
|
379 + | emp.output.push(',');
|
380 + | emp.write_string(&STRING, "Bob")?;
|
381 + | Ok(())
|
382 + | })?;
|
383 + | comp.output.push(',');
|
384 + | comp.output.push_str("\"metadata\":");
|
385 + | comp.write_map(&map_schema, |meta| {
|
386 + | meta.output.push_str("\"founded\":");
|
387 + | meta.write_integer(&INTEGER, 2010)?;
|
388 + | meta.output.push(',');
|
389 + | meta.output.push_str("\"size\":");
|
390 + | meta.write_integer(&INTEGER, 500)?;
|
391 + | Ok(())
|
392 + | })?;
|
393 + | comp.output.push(',');
|
394 + | comp.output.push_str("\"active\":");
|
395 + | comp.write_boolean(&BOOLEAN, true)?;
|
396 + | Ok(())
|
397 + | })?;
|
398 + | ls.output.push(',');
|
399 + | ls.write_struct(&struct_schema, |comp| {
|
400 + | comp.output.push_str("\"name\":");
|
401 + | comp.write_string(&STRING, "StartupInc")?;
|
402 + | comp.output.push(',');
|
403 + | comp.output.push_str("\"employees\":");
|
404 + | comp.write_list(&list_schema, |emp| {
|
405 + | emp.write_string(&STRING, "Charlie")?;
|
406 + | Ok(())
|
407 + | })?;
|
408 + | comp.output.push(',');
|
409 + | comp.output.push_str("\"metadata\":");
|
410 + | comp.write_map(&map_schema, |meta| {
|
411 + | meta.output.push_str("\"founded\":");
|
412 + | meta.write_integer(&INTEGER, 2020)?;
|
413 + | Ok(())
|
414 + | })?;
|
415 + | comp.output.push(',');
|
416 + | comp.output.push_str("\"active\":");
|
417 + | comp.write_boolean(&BOOLEAN, false)?;
|
418 + | Ok(())
|
419 + | })?;
|
420 + | Ok(())
|
421 + | })?;
|
422 + | s.output.push(',');
|
423 + | s.output.push_str("\"tags\":");
|
424 + | s.write_map(&map_schema, |tags| {
|
425 + | tags.output.push_str("\"role\":");
|
426 + | tags.write_string(&STRING, "admin")?;
|
427 + | tags.output.push(',');
|
428 + | tags.output.push_str("\"level\":");
|
429 + | tags.write_string(&STRING, "senior")?;
|
430 + | Ok(())
|
431 + | })?;
|
432 + | Ok(())
|
433 + | })
|
434 + | .unwrap();
|
435 + |
|
436 + | let output = String::from_utf8(ser.finish().unwrap()).unwrap();
|
437 + | let expected = r#"{"id":12345,"name":"John Doe","scores":[95.5,87.3,92.1],"address":{"street":"123 Main St","city":"Seattle","zip":98101},"companies":[{"name":"TechCorp","employees":["Alice","Bob"],"metadata":{"founded":2010,"size":500},"active":true},{"name":"StartupInc","employees":["Charlie"],"metadata":{"founded":2020},"active":false}],"tags":{"role":"admin","level":"senior"}}"#;
|
438 + | assert_eq!(output, expected);
|
439 + | }
|
440 + | }
|