use axum::http::{HeaderMap, StatusCode};
use bytes::Bytes;
use serde_json::Value;
use crate::error::AwsError;
use super::ParsedRequest;
pub fn parse_request(body: &Bytes) -> Result<ParsedRequest, AwsError> {
let body_str = std::str::from_utf8(body)
.map_err(|_| AwsError::bad_request("InvalidRequest", "Request body is not valid UTF-8"))?;
let params: Vec<(String, String)> = serde_urlencoded::from_str(body_str)
.map_err(|e| AwsError::bad_request("InvalidRequest", format!("Invalid form body: {e}")))?;
let operation = params
.iter()
.find(|(k, _)| k == "Action")
.map(|(_, v)| v.clone())
.ok_or_else(|| AwsError::bad_request("MissingAction", "Missing 'Action' parameter"))?;
let input = flatten_to_json(¶ms);
Ok(ParsedRequest { operation, input })
}
fn flatten_to_json(params: &[(String, String)]) -> Value {
let mut map = serde_json::Map::new();
for (key, value) in params {
if key == "Action" || key == "Version" {
continue;
}
set_nested(&mut map, key, value);
}
Value::Object(map)
}
fn set_nested(map: &mut serde_json::Map<String, Value>, key: &str, value: &str) {
let parts: Vec<&str> = key.split('.').collect();
set_nested_recursive(map, &parts, value);
}
fn set_nested_recursive(map: &mut serde_json::Map<String, Value>, parts: &[&str], value: &str) {
if parts.is_empty() {
return;
}
if parts.len() == 1 {
map.insert(parts[0].to_string(), Value::String(value.to_string()));
return;
}
let key = parts[0];
let rest = &parts[1..];
if let Some(next) = rest.first()
&& next.parse::<usize>().is_ok()
{
let entry = map
.entry(key.to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if let Value::Array(arr) = entry {
let idx: usize = next.parse::<usize>().unwrap() - 1; while arr.len() <= idx {
arr.push(Value::Object(serde_json::Map::new()));
}
if rest.len() > 1 {
if let Value::Object(ref mut inner) = arr[idx] {
set_nested_recursive(inner, &rest[1..], value);
}
} else {
arr[idx] = Value::String(value.to_string());
}
}
return;
}
let entry = map
.entry(key.to_string())
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if let Value::Object(inner) = entry {
set_nested_recursive(inner, rest, value);
}
}
pub fn serialize_response(
operation: &str,
output: &Value,
request_id: &str,
) -> (StatusCode, HeaderMap, Bytes) {
let result_xml = json_to_xml_fields(output);
let xml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<{operation}Response xmlns=\"https://iam.amazonaws.com/doc/2010-05-08/\">\n\
<{operation}Result>\n\
{result_xml}\
</{operation}Result>\n\
<ResponseMetadata>\n\
<RequestId>{request_id}</RequestId>\n\
</ResponseMetadata>\n\
</{operation}Response>"
);
let mut headers = HeaderMap::new();
headers.insert("content-type", "text/xml".parse().unwrap());
headers.insert("x-amzn-requestid", request_id.parse().unwrap());
(StatusCode::OK, headers, Bytes::from(xml))
}
pub fn serialize_error(error: &AwsError, request_id: &str) -> (StatusCode, HeaderMap, Bytes) {
let error_type = match error.error_type {
crate::error::ErrorType::Sender => "Sender",
crate::error::ErrorType::Receiver => "Receiver",
};
let xml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<ErrorResponse xmlns=\"http://iam.amazonaws.com/doc/2010-05-08/\">\n\
<Error>\n\
<Type>{error_type}</Type>\n\
<Code>{code}</Code>\n\
<Message>{message}</Message>\n\
</Error>\n\
<RequestId>{request_id}</RequestId>\n\
</ErrorResponse>",
code = error.code,
message = error.message,
);
let mut headers = HeaderMap::new();
headers.insert("content-type", "text/xml".parse().unwrap());
headers.insert("x-amzn-requestid", request_id.parse().unwrap());
(error.status, headers, Bytes::from(xml))
}
pub fn json_to_xml_fields(value: &Value) -> String {
match value {
Value::Object(map) => {
let mut xml = String::new();
for (key, val) in map {
match val {
Value::Object(_) => {
xml.push_str(&format!("<{key}>\n{}</{key}>\n", json_to_xml_fields(val)));
}
Value::Array(arr) => {
for item in arr {
xml.push_str(&format!(
"<{key}>\n{}</{key}>\n",
json_to_xml_fields(item)
));
}
}
Value::String(s) => {
xml.push_str(&format!("<{key}>{s}</{key}>\n"));
}
Value::Number(n) => {
xml.push_str(&format!("<{key}>{n}</{key}>\n"));
}
Value::Bool(b) => {
xml.push_str(&format!("<{key}>{b}</{key}>\n"));
}
Value::Null => {
xml.push_str(&format!("<{key}/>\n"));
}
}
}
xml
}
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_query() {
let body = Bytes::from("Action=GetCallerIdentity&Version=2011-06-15");
let result = parse_request(&body).unwrap();
assert_eq!(result.operation, "GetCallerIdentity");
assert_eq!(result.input, Value::Object(serde_json::Map::new()));
}
#[test]
fn test_parse_query_with_params() {
let body = Bytes::from("Action=CreateUser&UserName=testuser&Path=/engineering/");
let result = parse_request(&body).unwrap();
assert_eq!(result.operation, "CreateUser");
assert_eq!(result.input["UserName"], "testuser");
assert_eq!(result.input["Path"], "/engineering/");
}
#[test]
fn test_flatten_dot_notation() {
let params = vec![
("Action".to_string(), "TagResource".to_string()),
("Tags.member.1.Key".to_string(), "Env".to_string()),
("Tags.member.1.Value".to_string(), "prod".to_string()),
("Tags.member.2.Key".to_string(), "Team".to_string()),
("Tags.member.2.Value".to_string(), "eng".to_string()),
];
let result = flatten_to_json(¶ms);
let tags = result["Tags"]["member"].as_array().unwrap();
assert_eq!(tags.len(), 2);
assert_eq!(tags[0]["Key"], "Env");
assert_eq!(tags[0]["Value"], "prod");
assert_eq!(tags[1]["Key"], "Team");
assert_eq!(tags[1]["Value"], "eng");
}
}