1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
use crate::model::GeneratedModels;
use crate::reference::resolve_local_reference;
use crate::CodeGenerator;
use indexmap::IndexMap;
use openapiv3::{
ReferenceOr, Schema, SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty,
};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::borrow::Cow;
use std::collections::HashMap;
/// Represents a request or response body type.
pub struct BodySchema {
/// Type of the request or response body as passed into the request handler (for request bodies)
/// or stored in the response body enum variant.
pub required_type: TokenStream,
/// Code appended to the decoded request body to deserialize from `Option<Vec<u8>>` to
/// `Result<Option<#required_type>, EventError>`.
pub deserialize: TokenStream,
/// Code that takes a `body` variable of `required_type` and converts it to a
/// `aws_lambda_events::encodings::Body`.
pub serialize: TokenStream,
}
impl CodeGenerator {
/// Generates both request and response body schemas to ensure symmetry.
pub(crate) fn gen_body_schema(
&self,
schema_or_ref_opt: Option<&ReferenceOr<Schema>>,
mime_type: &str,
response_type: &str,
openapi_inline: &serde_yaml::Mapping,
components_schemas: &IndexMap<String, ReferenceOr<Schema>>,
generated_models: &HashMap<Ident, TokenStream>,
) -> BodySchema {
match (mime_type, schema_or_ref_opt) {
("application/json", None) => BodySchema {
required_type: quote! { serde_json::Value },
deserialize: quote! {
.map(|decoded_body|
serde_path_to_error::deserialize::<_, serde_json::Value>(
&mut serde_json::Deserializer::from_slice(&decoded_body)
)
)
.transpose()
.map_err(|err| EventError::InvalidBodyJson(Box::new(err), Backtrace::new()))
},
serialize: quote! { Body::Text(body.to_string()) },
},
("application/json", Some(schema_or_ref)) => {
let schema = match schema_or_ref {
ReferenceOr::Reference { reference } => {
Cow::Owned(resolve_local_reference::<Schema>(reference, openapi_inline).target)
}
ReferenceOr::Item(schema) => Cow::Borrowed(schema),
};
// If the body schema is a string instead of an object or reference (e.g., for
// webhook handlers that require the raw request body for HMAC verification), don't
// deserialize it. We also don't use any newtypes here, even if the user defined a named
// schema for this type.
if let SchemaKind::Type(Type::String(StringType {
enumeration,
format,
..
})) = &schema.as_ref().schema_kind
{
if !enumeration.is_empty() {
panic!("unexpected inline enum JSON request or response body: {schema:#?}");
}
match format {
// We assume that a binary type for a JSON request body wants the raw JSON as a byte
// string, since there's no well-defined JSON representation for binary data (e.g.,
// it could be an escaped string, a base64-encoded string, an array of byte integers,
// etc.).
VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => BodySchema {
required_type: quote! { Vec<u8> },
deserialize: quote! { .map(Ok).transpose() },
serialize: quote! { Body::Binary(body) },
},
// We assume that a string type for a JSON request body wants the raw JSON as a
// string, rather than expecting a JSON payload containing a quoted and escaped
// string. We just ignore any formats like date-time here, which wouldn't make much
// sense for an application/json MIME type. It's unclear what to do with newtypes
// (`VariantOrUnknownOrEmpty::Unknown`) here (i.e., should we use the FromStr or
// Deserialize trait for parsing?), so we just pass the raw string and let the user do
// any necessary deserialization.
VariantOrUnknownOrEmpty::Empty
| VariantOrUnknownOrEmpty::Item(_)
| VariantOrUnknownOrEmpty::Unknown(_) => BodySchema {
required_type: quote! { String },
deserialize: quote! {
.map(String::from_utf8)
.transpose()
.map_err(|err| EventError::InvalidBodyUtf8(Box::new(err), Backtrace::new()))
},
serialize: quote! { Body::Text(body) },
},
}
} else {
let (required_type, _) = self.inline_ref_or_schema(
schema_or_ref,
components_schemas,
GeneratedModels::Done(generated_models),
);
let deserialize = quote! {
.map(|decoded_body|
serde_path_to_error::deserialize::<_, #required_type>(
&mut serde_json::Deserializer::from_slice(&decoded_body)
)
)
.transpose()
.map_err(|err| EventError::InvalidBodyJson(Box::new(err), Backtrace::new()))
};
let serialize = quote! {
Body::Text(
to_json(&body)
.map_err(|err| {
EventError::ToJsonResponse {
type_name: std::borrow::Cow::Borrowed(#response_type),
source: Box::new(err),
backtrace: Backtrace::new()
}
})?
)
};
BodySchema {
required_type,
deserialize,
serialize,
}
}
}
// If there's a schema defined for these flat types, we just ignore it since there's
// no well-defined way to (de)serialize to them. It becomes the user's responsibility to
// do the (de)serialization.
("application/octet-stream", _) => BodySchema {
required_type: quote! { Vec<u8> },
deserialize: quote! { .map(Ok).transpose() },
serialize: quote! { Body::Binary(body) },
},
// Treat all text types as UTF-8 strings.
(mime, _) if mime.starts_with("text/") => BodySchema {
required_type: quote! { String },
deserialize: quote! {
.map(String::from_utf8)
.transpose()
.map_err(|err| EventError::InvalidBodyUtf8(Box::new(err), Backtrace::new()))
},
serialize: quote! { Body::Text(body) },
},
// Any types we don't explicitly support we just leave as raw byte strings.
_ => BodySchema {
required_type: quote! { Vec<u8> },
deserialize: quote! { .map(Ok).transpose() },
serialize: quote! { Body::Binary(body) },
},
}
}
}