rustack_ses_http/
response.rs1use rustack_ses_model::error::SesError;
20
21use crate::body::SesResponseBody;
22
23pub const XML_CONTENT_TYPE: &str = "text/xml";
25
26pub const JSON_CONTENT_TYPE: &str = "application/json";
28
29const XML_NS: &str = "http://ses.amazonaws.com/doc/2010-12-01/";
31
32#[must_use]
34pub fn xml_response(xml: String, request_id: &str) -> http::Response<SesResponseBody> {
35 let body = SesResponseBody::from_xml(xml.into_bytes());
36 http::Response::builder()
37 .status(http::StatusCode::OK)
38 .header("content-type", XML_CONTENT_TYPE)
39 .header("x-amzn-requestid", request_id)
40 .body(body)
41 .expect("valid XML response")
42}
43
44#[must_use]
46pub fn json_response(json: String, status: http::StatusCode) -> http::Response<SesResponseBody> {
47 let body = SesResponseBody::from_json(json);
48 http::Response::builder()
49 .status(status)
50 .header("content-type", JSON_CONTENT_TYPE)
51 .body(body)
52 .expect("valid JSON response")
53}
54
55#[must_use]
57pub fn error_to_xml(error: &SesError, request_id: &str) -> String {
58 format!(
59 "<ErrorResponse \
60 xmlns=\"{XML_NS}\"><Error><Type>{}</Type><Code>{}</Code><Message>{}</Message></\
61 Error><RequestId>{}</RequestId></ErrorResponse>",
62 error.code.fault(),
63 error.code.code(),
64 xml_escape(&error.message),
65 xml_escape(request_id),
66 )
67}
68
69#[must_use]
71pub fn error_to_response(error: &SesError, request_id: &str) -> http::Response<SesResponseBody> {
72 let xml = error_to_xml(error, request_id);
73 let body = SesResponseBody::from_xml(xml.into_bytes());
74 http::Response::builder()
75 .status(error.status_code)
76 .header("content-type", XML_CONTENT_TYPE)
77 .header("x-amzn-requestid", request_id)
78 .body(body)
79 .expect("valid error response")
80}
81
82#[must_use]
84pub fn error_to_json_response(error: &SesError) -> http::Response<SesResponseBody> {
85 let json = serde_json::json!({
86 "__type": error.code.code(),
87 "message": error.message,
88 });
89 let body = SesResponseBody::from_json(json.to_string());
90 http::Response::builder()
91 .status(error.status_code)
92 .header("content-type", JSON_CONTENT_TYPE)
93 .body(body)
94 .expect("valid JSON error response")
95}
96
97#[must_use]
101pub fn xml_escape(s: &str) -> String {
102 if !s.contains(['&', '<', '>', '"', '\'']) {
103 return s.to_owned();
104 }
105
106 let mut result = String::with_capacity(s.len() + 16);
107 for ch in s.chars() {
108 match ch {
109 '&' => result.push_str("&"),
110 '<' => result.push_str("<"),
111 '>' => result.push_str(">"),
112 '"' => result.push_str("""),
113 '\'' => result.push_str("'"),
114 _ => result.push(ch),
115 }
116 }
117 result
118}
119
120#[derive(Debug)]
122pub struct XmlWriter {
123 buf: String,
124}
125
126impl Default for XmlWriter {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl XmlWriter {
133 #[must_use]
135 pub fn new() -> Self {
136 Self {
137 buf: String::with_capacity(512),
138 }
139 }
140
141 pub fn start_response(&mut self, operation: &str) {
143 self.buf.push('<');
144 self.buf.push_str(operation);
145 self.buf.push_str("Response xmlns=\"");
146 self.buf.push_str(XML_NS);
147 self.buf.push_str("\">");
148 }
149
150 pub fn start_result(&mut self, operation: &str) {
152 self.buf.push('<');
153 self.buf.push_str(operation);
154 self.buf.push_str("Result>");
155 }
156
157 pub fn end_element(&mut self, name: &str) {
159 self.buf.push_str("</");
160 self.buf.push_str(name);
161 self.buf.push('>');
162 }
163
164 pub fn write_element(&mut self, name: &str, value: &str) {
166 self.buf.push('<');
167 self.buf.push_str(name);
168 self.buf.push('>');
169 self.buf.push_str(&xml_escape(value));
170 self.buf.push_str("</");
171 self.buf.push_str(name);
172 self.buf.push('>');
173 }
174
175 pub fn write_optional_element(&mut self, name: &str, value: Option<&str>) {
177 if let Some(v) = value {
178 self.write_element(name, v);
179 }
180 }
181
182 pub fn write_bool_element(&mut self, name: &str, value: bool) {
184 self.write_element(name, if value { "true" } else { "false" });
185 }
186
187 pub fn write_f64_element(&mut self, name: &str, value: f64) {
189 self.write_element(name, &value.to_string());
190 }
191
192 pub fn write_i64_element(&mut self, name: &str, value: i64) {
194 self.write_element(name, &value.to_string());
195 }
196
197 pub fn write_response_metadata(&mut self, request_id: &str) {
199 self.buf.push_str("<ResponseMetadata>");
200 self.write_element("RequestId", request_id);
201 self.buf.push_str("</ResponseMetadata>");
202 }
203
204 pub fn raw(&mut self, s: &str) {
206 self.buf.push_str(s);
207 }
208
209 #[must_use]
211 pub fn into_string(self) -> String {
212 self.buf
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_should_escape_xml_special_chars() {
222 assert_eq!(xml_escape("hello"), "hello");
223 assert_eq!(xml_escape("a & b"), "a & b");
224 assert_eq!(xml_escape("<tag>"), "<tag>");
225 }
226
227 #[test]
228 fn test_should_format_error_xml() {
229 let err = SesError::message_rejected("Email address is not verified.");
230 let xml = error_to_xml(&err, "req-123");
231 assert!(xml.contains("<Code>MessageRejected</Code>"));
232 assert!(xml.contains("Email address is not verified."));
233 assert!(xml.contains("<RequestId>req-123</RequestId>"));
234 }
235
236 #[test]
237 fn test_should_build_error_response_with_correct_status() {
238 let err = SesError::template_does_not_exist("my-template");
239 let resp = error_to_response(&err, "test-req");
240 assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
241 }
242
243 #[test]
244 fn test_should_build_xml_with_writer() {
245 let mut w = XmlWriter::new();
246 w.start_response("SendEmail");
247 w.start_result("SendEmail");
248 w.write_element("MessageId", "test-id-123");
249 w.end_element("SendEmailResult");
250 w.write_response_metadata("req-789");
251 w.end_element("SendEmailResponse");
252 let xml = w.into_string();
253 assert!(xml.contains("<MessageId>test-id-123</MessageId>"));
254 assert!(xml.contains("<RequestId>req-789</RequestId>"));
255 assert!(xml.contains("xmlns=\"http://ses.amazonaws.com/doc/2010-12-01/\""));
256 }
257
258 #[test]
259 fn test_should_build_json_error_response() {
260 let err = SesError::internal_error("Something went wrong");
261 let resp = error_to_json_response(&err);
262 assert_eq!(resp.status(), http::StatusCode::INTERNAL_SERVER_ERROR);
263 assert_eq!(
264 resp.headers().get("content-type").unwrap(),
265 JSON_CONTENT_TYPE,
266 );
267 }
268
269 #[test]
270 fn test_should_build_success_json_response() {
271 let resp = json_response("{\"ok\":true}".to_owned(), http::StatusCode::OK);
272 assert_eq!(resp.status(), http::StatusCode::OK);
273 assert_eq!(
274 resp.headers().get("content-type").unwrap(),
275 JSON_CONTENT_TYPE,
276 );
277 }
278}