use super::ProtocolHandler;
use crate::aws::client::AwsClients;
use crate::resource::path_extractor::{extract_by_path, extract_list};
use crate::resource::protocol::ApiConfig;
use anyhow::Result;
use serde_json::Value;
pub struct JsonProtocolHandler;
impl JsonProtocolHandler {
pub async fn execute_impl(
&self,
clients: &AwsClients,
service: &str,
config: &ApiConfig,
params: &Value,
) -> Result<String> {
let action = config
.action
.as_ref()
.ok_or_else(|| anyhow::anyhow!("JSON protocol requires 'action' field"))?;
let mut body = serde_json::Map::new();
for (key, value) in &config.static_params {
body.insert(key.clone(), value.clone());
}
if let Value::Object(map) = params {
for (key, value) in map {
if !key.starts_with('_') {
let mapped_key = config
.param_mapping
.get(key)
.cloned()
.unwrap_or_else(|| key.clone());
let unwrapped_value = match value {
Value::Array(arr) if arr.len() == 1 => arr[0].clone(),
_ => value.clone(),
};
body.insert(mapped_key, unwrapped_value);
}
}
}
if let Some(pagination) = &config.pagination {
if let Some(max_param) = &pagination.max_results_param {
let max_value = pagination.max_results.unwrap_or(100);
body.insert(max_param.clone(), Value::from(max_value));
}
if let Some(token) = params.get("_page_token").and_then(|v| v.as_str()) {
if let Some(input_token) = &pagination.input_token {
body.insert(input_token.clone(), Value::String(token.to_string()));
}
}
}
let body_str = serde_json::to_string(&Value::Object(body))?;
clients.http.json_request(service, action, &body_str).await
}
}
impl ProtocolHandler for JsonProtocolHandler {
fn parse_items(
&self,
response: &str,
config: &ApiConfig,
) -> Result<(Vec<Value>, Option<String>)> {
let json: Value = serde_json::from_str(response)?;
let items = if let Some(root) = &config.response_root {
extract_list(&json, root)
} else {
if let Some(arr) = json.as_array() {
arr.clone()
} else {
vec![json.clone()]
}
};
let next_token = config
.pagination
.as_ref()
.and_then(|p| p.output_token.as_ref())
.and_then(|path| {
let token = extract_by_path(&json, path);
token.as_str().map(|s| s.to_string())
});
Ok((items, next_token))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dynamodb_response() {
let handler = JsonProtocolHandler;
let response = r#"{
"TableNames": ["table1", "table2", "table3"]
}"#;
let config = ApiConfig {
response_root: Some("/TableNames".to_string()),
..Default::default()
};
let (items, _) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 3);
assert_eq!(items[0], "table1");
}
#[test]
fn test_parse_ecs_clusters_response() {
let response = r#"{
"clusters": [
{"clusterArn": "arn:aws:ecs:us-east-1:123:cluster/default", "status": "ACTIVE"},
{"clusterArn": "arn:aws:ecs:us-east-1:123:cluster/prod", "status": "ACTIVE"}
]
}"#;
let config = ApiConfig {
response_root: Some("/clusters".to_string()),
..Default::default()
};
let handler = JsonProtocolHandler;
let (items, _) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(
items[0]["clusterArn"],
"arn:aws:ecs:us-east-1:123:cluster/default"
);
}
#[test]
fn test_parse_with_pagination() {
let response = r#"{
"clusters": [{"name": "test"}],
"nextToken": "abc123"
}"#;
let config = ApiConfig {
response_root: Some("/clusters".to_string()),
pagination: Some(crate::resource::protocol::PaginationConfig {
output_token: Some("/nextToken".to_string()),
..Default::default()
}),
..Default::default()
};
let handler = JsonProtocolHandler;
let (items, next_token) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(next_token, Some("abc123".to_string()));
}
}