mod transport;
mod xml;
use ciborium::Value as CborValue;
use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use vantage_core::{Result, error};
use vantage_types::Record;
use crate::account::AwsAccount;
use crate::condition::{AwsCondition, build_query_form};
use crate::dispatch::{OperationDescriptor, json_to_cbor};
pub(crate) use transport::query_call;
pub(crate) async fn execute(
account: &AwsAccount,
op: &OperationDescriptor<'_>,
resolved: &[AwsCondition],
) -> Result<JsonValue> {
let (version, action) = split_target(op.target)?;
let mut form = vec![
("Action".to_string(), action.to_string()),
("Version".to_string(), version.to_string()),
];
form.extend(build_query_form(resolved)?);
let xml = query_call(account, op.service, &form).await?;
xml::parse_query_response(&xml)
}
pub(crate) fn parse_records(
op: &OperationDescriptor<'_>,
resp: JsonValue,
id_field: Option<&str>,
) -> Result<IndexMap<String, Record<CborValue>>> {
let array = match resp.get(op.array_key) {
Some(JsonValue::Array(a)) => a.clone(),
Some(JsonValue::String(s)) if s.is_empty() => Vec::new(),
Some(other) => {
return Err(error!(
"AWS Query response array key has unexpected shape — \
expected a list of <member> elements",
array_key = op.array_key,
got = format!("{:?}", other),
body = format!("{}", resp)
));
}
None => {
return Err(error!(
"AWS Query response missing expected array key",
array_key = op.array_key,
body = format!("{}", resp)
));
}
};
let scalar_field = id_field.unwrap_or("value");
let mut out = IndexMap::with_capacity(array.len());
for (idx, item) in array.into_iter().enumerate() {
let obj = match item {
JsonValue::Object(map) => map,
JsonValue::String(_) | JsonValue::Number(_) => {
let mut m = serde_json::Map::new();
m.insert(scalar_field.to_string(), item);
m
}
other => {
return Err(error!(
"AWS Query response array entry is not an object or scalar",
index = idx,
got = format!("{:?}", other)
));
}
};
let id = id_field
.and_then(|f| obj.get(f))
.and_then(|v| match v {
JsonValue::String(s) => Some(s.clone()),
JsonValue::Number(n) => Some(n.to_string()),
_ => None,
})
.unwrap_or_else(|| idx.to_string());
let record: Record<CborValue> =
obj.into_iter().map(|(k, v)| (k, json_to_cbor(v))).collect();
out.insert(id, record);
}
Ok(out)
}
fn split_target(target: &str) -> Result<(&str, &str)> {
target.split_once('.').ok_or_else(|| {
error!(
"Query target must be \"VERSION.Action\" (e.g. \"2010-05-08.ListUsers\") — got",
target = target
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn target_splits_into_version_and_action() {
let (v, a) = split_target("2010-05-08.ListUsers").unwrap();
assert_eq!(v, "2010-05-08");
assert_eq!(a, "ListUsers");
}
#[test]
fn target_without_dot_errors() {
assert!(split_target("ListUsers").is_err());
}
}