use rustack_ses_model::error::SesError;
use crate::body::SesResponseBody;
pub const XML_CONTENT_TYPE: &str = "text/xml";
pub const JSON_CONTENT_TYPE: &str = "application/json";
const XML_NS: &str = "http://ses.amazonaws.com/doc/2010-12-01/";
#[must_use]
pub fn xml_response(xml: String, request_id: &str) -> http::Response<SesResponseBody> {
let body = SesResponseBody::from_xml(xml.into_bytes());
http::Response::builder()
.status(http::StatusCode::OK)
.header("content-type", XML_CONTENT_TYPE)
.header("x-amzn-requestid", request_id)
.body(body)
.expect("valid XML response")
}
#[must_use]
pub fn json_response(json: String, status: http::StatusCode) -> http::Response<SesResponseBody> {
let body = SesResponseBody::from_json(json);
http::Response::builder()
.status(status)
.header("content-type", JSON_CONTENT_TYPE)
.body(body)
.expect("valid JSON response")
}
#[must_use]
pub fn error_to_xml(error: &SesError, request_id: &str) -> String {
format!(
"<ErrorResponse \
xmlns=\"{XML_NS}\"><Error><Type>{}</Type><Code>{}</Code><Message>{}</Message></\
Error><RequestId>{}</RequestId></ErrorResponse>",
error.code.fault(),
error.code.code(),
xml_escape(&error.message),
xml_escape(request_id),
)
}
#[must_use]
pub fn error_to_response(error: &SesError, request_id: &str) -> http::Response<SesResponseBody> {
let xml = error_to_xml(error, request_id);
let body = SesResponseBody::from_xml(xml.into_bytes());
http::Response::builder()
.status(error.status_code)
.header("content-type", XML_CONTENT_TYPE)
.header("x-amzn-requestid", request_id)
.body(body)
.expect("valid error response")
}
#[must_use]
pub fn error_to_json_response(error: &SesError) -> http::Response<SesResponseBody> {
let json = serde_json::json!({
"__type": error.code.code(),
"message": error.message,
});
let body = SesResponseBody::from_json(json.to_string());
http::Response::builder()
.status(error.status_code)
.header("content-type", JSON_CONTENT_TYPE)
.body(body)
.expect("valid JSON error response")
}
#[must_use]
pub fn xml_escape(s: &str) -> String {
if !s.contains(['&', '<', '>', '"', '\'']) {
return s.to_owned();
}
let mut result = String::with_capacity(s.len() + 16);
for ch in s.chars() {
match ch {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
_ => result.push(ch),
}
}
result
}
#[derive(Debug)]
pub struct XmlWriter {
buf: String,
}
impl Default for XmlWriter {
fn default() -> Self {
Self::new()
}
}
impl XmlWriter {
#[must_use]
pub fn new() -> Self {
Self {
buf: String::with_capacity(512),
}
}
pub fn start_response(&mut self, operation: &str) {
self.buf.push('<');
self.buf.push_str(operation);
self.buf.push_str("Response xmlns=\"");
self.buf.push_str(XML_NS);
self.buf.push_str("\">");
}
pub fn start_result(&mut self, operation: &str) {
self.buf.push('<');
self.buf.push_str(operation);
self.buf.push_str("Result>");
}
pub fn end_element(&mut self, name: &str) {
self.buf.push_str("</");
self.buf.push_str(name);
self.buf.push('>');
}
pub fn write_element(&mut self, name: &str, value: &str) {
self.buf.push('<');
self.buf.push_str(name);
self.buf.push('>');
self.buf.push_str(&xml_escape(value));
self.buf.push_str("</");
self.buf.push_str(name);
self.buf.push('>');
}
pub fn write_optional_element(&mut self, name: &str, value: Option<&str>) {
if let Some(v) = value {
self.write_element(name, v);
}
}
pub fn write_bool_element(&mut self, name: &str, value: bool) {
self.write_element(name, if value { "true" } else { "false" });
}
pub fn write_f64_element(&mut self, name: &str, value: f64) {
self.write_element(name, &value.to_string());
}
pub fn write_i64_element(&mut self, name: &str, value: i64) {
self.write_element(name, &value.to_string());
}
pub fn write_response_metadata(&mut self, request_id: &str) {
self.buf.push_str("<ResponseMetadata>");
self.write_element("RequestId", request_id);
self.buf.push_str("</ResponseMetadata>");
}
pub fn raw(&mut self, s: &str) {
self.buf.push_str(s);
}
#[must_use]
pub fn into_string(self) -> String {
self.buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_escape_xml_special_chars() {
assert_eq!(xml_escape("hello"), "hello");
assert_eq!(xml_escape("a & b"), "a & b");
assert_eq!(xml_escape("<tag>"), "<tag>");
}
#[test]
fn test_should_format_error_xml() {
let err = SesError::message_rejected("Email address is not verified.");
let xml = error_to_xml(&err, "req-123");
assert!(xml.contains("<Code>MessageRejected</Code>"));
assert!(xml.contains("Email address is not verified."));
assert!(xml.contains("<RequestId>req-123</RequestId>"));
}
#[test]
fn test_should_build_error_response_with_correct_status() {
let err = SesError::template_does_not_exist("my-template");
let resp = error_to_response(&err, "test-req");
assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
}
#[test]
fn test_should_build_xml_with_writer() {
let mut w = XmlWriter::new();
w.start_response("SendEmail");
w.start_result("SendEmail");
w.write_element("MessageId", "test-id-123");
w.end_element("SendEmailResult");
w.write_response_metadata("req-789");
w.end_element("SendEmailResponse");
let xml = w.into_string();
assert!(xml.contains("<MessageId>test-id-123</MessageId>"));
assert!(xml.contains("<RequestId>req-789</RequestId>"));
assert!(xml.contains("xmlns=\"http://ses.amazonaws.com/doc/2010-12-01/\""));
}
#[test]
fn test_should_build_json_error_response() {
let err = SesError::internal_error("Something went wrong");
let resp = error_to_json_response(&err);
assert_eq!(resp.status(), http::StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
resp.headers().get("content-type").unwrap(),
JSON_CONTENT_TYPE,
);
}
#[test]
fn test_should_build_success_json_response() {
let resp = json_response("{\"ok\":true}".to_owned(), http::StatusCode::OK);
assert_eq!(resp.status(), http::StatusCode::OK);
assert_eq!(
resp.headers().get("content-type").unwrap(),
JSON_CONTENT_TYPE,
);
}
}