use fakecloud_core::service::{AwsResponse, ResponseBody};
use serde_json::{Map, Value};
use quick_xml::events::Event;
use quick_xml::reader::Reader;
const JSON_CONTENT_TYPE: &str = "application/x-amz-json-1.0";
const NUMERIC_TAGS: &[&str] = &[
"Period",
"EvaluationPeriods",
"DatapointsToAlarm",
"Threshold",
"Duration",
"Size",
"ActionsSuppressorWaitPeriod",
"ActionsSuppressorExtensionPeriod",
"StorageResolution",
"SampleCount",
"Average",
"Sum",
"Minimum",
"Maximum",
"Values",
"Counts",
"ExtendedStatistics",
];
const LIST_TAGS: &[&str] = &[
"Datapoints",
"Metrics",
"Dimensions",
"MetricAlarms",
"CompositeAlarms",
"AnomalyDetectors",
"AnomalyDetectorTypes",
"InsightRules",
"ManagedRules",
"MetricStreams",
"Entries",
"MetricDataResults",
"Values",
"Counts",
"Timestamps",
"Tags",
"Messages",
"AlarmHistoryItems",
"DashboardEntries",
"ExcludedTimeRanges",
"OKActions",
"AlarmActions",
"InsufficientDataActions",
"MetricDataQueries",
"Datasets",
];
const BOOL_TAGS: &[&str] = &[
"ActionsEnabled",
"IncludeLinkedAccountsMetrics",
"ApplyOnTransformedLogs",
"ReturnData",
"PeriodicSpikes",
];
const TIMESTAMP_TAGS: &[&str] = &[
"Timestamp",
"Timestamps",
"AlarmConfigurationUpdatedTimestamp",
"StateUpdatedTimestamp",
"StateTransitionedTimestamp",
"LastModified",
"LastUpdateDate",
"LastUpdatedTimestamp",
"CreationDate",
"StartDate",
"ExpireDate",
"StartTime",
"EndTime",
];
pub(crate) fn xml_response_to_json(resp: AwsResponse) -> AwsResponse {
let status = resp.status;
let ResponseBody::Bytes(bytes) = resp.body else {
return resp;
};
let value = xml_to_json(&bytes);
let body = serde_json::to_vec(&value).unwrap_or_else(|_| b"{}".to_vec());
AwsResponse {
status,
content_type: JSON_CONTENT_TYPE.to_string(),
body: ResponseBody::Bytes(body.into()),
headers: resp.headers,
}
}
#[derive(Debug, Default)]
struct El {
name: String,
text: String,
children: Vec<El>,
}
fn xml_to_json(xml: &[u8]) -> Value {
let Some(root) = parse_xml(xml) else {
return Value::Object(Map::new());
};
let Some(result) = root.children.iter().find(|c| c.name.ends_with("Result")) else {
return Value::Object(Map::new());
};
match convert(result, "") {
Some(v @ Value::Object(_)) => v,
_ => Value::Object(Map::new()),
}
}
fn parse_xml(xml: &[u8]) -> Option<El> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut stack: Vec<El> = Vec::new();
let mut root: Option<El> = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = local_name(e.name().as_ref());
stack.push(El {
name,
..Default::default()
});
}
Ok(Event::Empty(e)) => {
let name = local_name(e.name().as_ref());
let el = El {
name,
..Default::default()
};
match stack.last_mut() {
Some(parent) => parent.children.push(el),
None => root = Some(el),
}
}
Ok(Event::Text(e)) => {
if let Some(top) = stack.last_mut() {
if let Ok(text) = e.unescape() {
top.text.push_str(text.as_ref());
}
}
}
Ok(Event::CData(e)) => {
if let Some(top) = stack.last_mut() {
if let Ok(s) = std::str::from_utf8(e.as_ref()) {
top.text.push_str(s);
}
}
}
Ok(Event::End(_)) => {
if let Some(done) = stack.pop() {
match stack.last_mut() {
Some(parent) => parent.children.push(done),
None => root = Some(done),
}
}
}
Ok(Event::Eof) => break,
Ok(_) => {}
Err(_) => return None,
}
buf.clear();
}
root
}
fn local_name(raw: &[u8]) -> String {
let s = String::from_utf8_lossy(raw);
match s.rsplit_once(':') {
Some((_, local)) => local.to_string(),
None => s.into_owned(),
}
}
fn convert(el: &El, parent_tag: &str) -> Option<Value> {
if el.children.is_empty() {
let text = el.text.trim();
if text.is_empty() {
if LIST_TAGS.contains(&el.name.as_str()) {
return Some(Value::Array(Vec::new()));
}
return None;
}
let typing_tag = if el.name == "member" || el.name == "value" {
parent_tag
} else {
&el.name
};
return Some(type_leaf(typing_tag, text));
}
if el.children.iter().all(|c| c.name == "member") {
let arr: Vec<Value> = el
.children
.iter()
.filter_map(|m| convert(m, &el.name))
.collect();
return Some(Value::Array(arr));
}
if el.children.iter().all(|c| c.name == "entry") {
let mut obj = Map::new();
for entry in &el.children {
let key = entry
.children
.iter()
.find(|c| c.name == "key")
.map(|k| k.text.trim().to_string());
let val = entry
.children
.iter()
.find(|c| c.name == "value")
.and_then(|v| convert(v, &el.name));
if let (Some(k), Some(v)) = (key, val) {
obj.insert(k, v);
}
}
return Some(Value::Object(obj));
}
let mut obj = Map::new();
for child in &el.children {
if let Some(v) = convert(child, &el.name) {
obj.insert(child.name.clone(), v);
}
}
Some(Value::Object(obj))
}
fn type_leaf(tag: &str, text: &str) -> Value {
if TIMESTAMP_TAGS.contains(&tag) {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(text) {
let secs = dt.timestamp_millis() as f64 / 1000.0;
if let Some(n) = serde_json::Number::from_f64(secs) {
return Value::Number(n);
}
}
return Value::String(text.to_string());
}
if BOOL_TAGS.contains(&tag) {
return match text {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
other => Value::String(other.to_string()),
};
}
if NUMERIC_TAGS.contains(&tag) {
if !text.contains('.') && !text.contains('e') && !text.contains('E') {
if let Ok(i) = text.parse::<i64>() {
return Value::Number(i.into());
}
}
if let Ok(f) = text.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
return Value::Number(n);
}
}
return Value::String(text.to_string());
}
Value::String(text.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn json(action: &str, inner: &str) -> Value {
let xml = fakecloud_core::query::query_response_xml(
action,
"http://monitoring.amazonaws.com/doc/2010-08-01/",
inner,
"req-1",
);
xml_to_json(xml.as_bytes())
}
#[test]
fn metadata_only_becomes_empty_object() {
let xml = fakecloud_core::query::query_metadata_only_xml(
"PutMetricData",
"http://monitoring.amazonaws.com/doc/2010-08-01/",
"req-1",
);
assert_eq!(xml_to_json(xml.as_bytes()), serde_json::json!({}));
}
#[test]
fn list_metrics_members_become_array() {
let inner = "<Metrics><member><Namespace>AWS/EC2</Namespace>\
<MetricName>CPUUtilization</MetricName>\
<Dimensions><member><Name>InstanceId</Name><Value>i-1</Value></member></Dimensions>\
</member></Metrics><NextToken>tok</NextToken>";
let v = json("ListMetrics", inner);
assert_eq!(v["Metrics"][0]["Namespace"], "AWS/EC2");
assert_eq!(v["Metrics"][0]["MetricName"], "CPUUtilization");
assert_eq!(v["Metrics"][0]["Dimensions"][0]["Name"], "InstanceId");
assert_eq!(v["Metrics"][0]["Dimensions"][0]["Value"], "i-1");
assert_eq!(v["NextToken"], "tok");
assert!(v["Metrics"].is_array());
}
#[test]
fn datapoints_are_typed() {
let inner = "<Label>CPUUtilization</Label><Datapoints><member>\
<Timestamp>2020-01-01T00:00:00.000Z</Timestamp>\
<Average>42.5</Average><SampleCount>3</SampleCount><Unit>Percent</Unit>\
</member></Datapoints>";
let v = json("GetMetricStatistics", inner);
assert_eq!(v["Label"], "CPUUtilization");
assert_eq!(v["Datapoints"][0]["Average"], 42.5);
assert_eq!(v["Datapoints"][0]["SampleCount"], 3);
assert_eq!(v["Datapoints"][0]["Unit"], "Percent");
assert_eq!(v["Datapoints"][0]["Timestamp"], 1577836800.0);
}
#[test]
fn alarm_flags_typed() {
let inner = "<MetricAlarms><member><AlarmName>cpu</AlarmName>\
<ActionsEnabled>true</ActionsEnabled><Threshold>80.0</Threshold>\
<EvaluationPeriods>2</EvaluationPeriods></member></MetricAlarms>";
let v = json("DescribeAlarms", inner);
assert_eq!(v["MetricAlarms"][0]["AlarmName"], "cpu");
assert_eq!(v["MetricAlarms"][0]["ActionsEnabled"], true);
assert_eq!(v["MetricAlarms"][0]["Threshold"], 80.0);
assert_eq!(v["MetricAlarms"][0]["EvaluationPeriods"], 2);
}
#[test]
fn timestamps_member_list_typed() {
let inner = "<MetricDataResults><member><Id>m1</Id>\
<Timestamps><member>2020-01-01T00:00:00.000Z</member></Timestamps>\
<Values><member>1.5</member></Values></member></MetricDataResults>";
let v = json("GetMetricData", inner);
assert_eq!(v["MetricDataResults"][0]["Timestamps"][0], 1577836800.0);
assert_eq!(v["MetricDataResults"][0]["Values"][0], 1.5);
}
#[test]
fn empty_list_container_becomes_empty_array() {
let inner = "<Label>cpu</Label><Datapoints></Datapoints><NextToken></NextToken>";
let v = json("GetMetricStatistics", inner);
assert_eq!(v["Label"], "cpu");
assert_eq!(v["Datapoints"], serde_json::json!([]));
assert!(v["Datapoints"].is_array());
assert!(
v.get("NextToken").is_none(),
"empty scalar should be omitted, got {v}"
);
}
#[test]
fn empty_tags_and_messages_lists_become_empty_arrays() {
let tags = json("ListTagsForResource", "<Tags></Tags>");
assert_eq!(tags["Tags"], serde_json::json!([]));
let inner = "<MetricDataResults><member><Id>m1</Id>\
<Messages></Messages><Values></Values></member></MetricDataResults>";
let v = json("GetMetricData", inner);
assert_eq!(v["MetricDataResults"][0]["Messages"], serde_json::json!([]));
assert_eq!(v["MetricDataResults"][0]["Values"], serde_json::json!([]));
}
}