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 RestJsonProtocolHandler;
impl RestJsonProtocolHandler {
pub async fn execute_impl(
&self,
clients: &AwsClients,
service: &str,
config: &ApiConfig,
params: &Value,
) -> Result<String> {
let method = config.method.as_deref().unwrap_or("GET");
let path_template = config
.path
.as_ref()
.ok_or_else(|| anyhow::anyhow!("REST-JSON protocol requires 'path' field"))?;
let mut path = path_template.clone();
if let Value::Object(map) = params {
for (key, value) in map {
if let Some(s) = value.as_str() {
path = path.replace(&format!("{{{}}}", key), s);
}
}
}
if method == "GET" {
let mut query_parts: Vec<String> = Vec::new();
if let Some(token) = params.get("_page_token").and_then(|v| v.as_str()) {
if let Some(pagination) = &config.pagination {
if let Some(input_token) = &pagination.input_token {
query_parts.push(format!("{}={}", input_token, urlencoding::encode(token)));
}
}
}
if let Some(pagination) = &config.pagination {
if let Some(max_param) = &pagination.max_results_param {
let max_value = pagination.max_results.unwrap_or(50);
query_parts.push(format!("{}={}", max_param, max_value));
}
}
if !query_parts.is_empty() {
if path.contains('?') {
path = format!("{}&{}", path, query_parts.join("&"));
} else {
path = format!("{}?{}", path, query_parts.join("&"));
}
}
}
let body = if method != "GET" {
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 unwrapped_value = match value {
Value::Array(arr) if arr.len() == 1 => arr[0].clone(),
_ => value.clone(),
};
body.insert(key.clone(), unwrapped_value);
}
}
}
if body.is_empty() {
None
} else {
Some(serde_json::to_string(&Value::Object(body))?)
}
} else {
None
};
clients
.http
.rest_json_request(service, method, &path, body.as_deref())
.await
}
}
impl ProtocolHandler for RestJsonProtocolHandler {
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_lambda_functions_response() {
let response = r#"{
"Functions": [
{"FunctionName": "func1", "Runtime": "nodejs18.x"},
{"FunctionName": "func2", "Runtime": "python3.9"}
],
"NextMarker": "next123"
}"#;
let config = ApiConfig {
response_root: Some("/Functions".to_string()),
pagination: Some(crate::resource::protocol::PaginationConfig {
output_token: Some("/NextMarker".to_string()),
..Default::default()
}),
..Default::default()
};
let handler = RestJsonProtocolHandler;
let (items, next_token) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0]["FunctionName"], "func1");
assert_eq!(next_token, Some("next123".to_string()));
}
#[test]
fn test_parse_eks_clusters_response() {
let response = r#"{
"clusters": ["cluster1", "cluster2"]
}"#;
let config = ApiConfig {
response_root: Some("/clusters".to_string()),
..Default::default()
};
let handler = RestJsonProtocolHandler;
let (items, _) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0], "cluster1");
}
#[test]
fn test_parse_redshift_clusters_response() {
let response = r#"{
"clusters": [
{
"ClusterIdentifier": "my-cluster",
"ClusterStatus": "available",
"NodeType": "dc2.large",
"NumberOfNodes": 2,
"DBName": "mydb",
"ClusterVersion": "1.0"
},
{
"ClusterIdentifier": "my-cluster-2",
"ClusterStatus": "paused",
"NodeType": "ra3.xlplus",
"NumberOfNodes": 4,
"DBName": "devdb",
"ClusterVersion": "1.0"
}
],
"NextToken": "next123"
}"#;
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 = RestJsonProtocolHandler;
let (items, next_token) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0]["ClusterIdentifier"], "my-cluster");
assert_eq!(items[0]["ClusterStatus"], "available");
assert_eq!(items[0]["NodeType"], "dc2.large");
assert_eq!(items[0]["NumberOfNodes"], 2);
assert_eq!(items[0]["DBName"], "mydb");
assert_eq!(items[0]["ClusterVersion"], "1.0");
assert_eq!(items[1]["ClusterIdentifier"], "my-cluster-2");
assert_eq!(items[1]["ClusterStatus"], "paused");
assert_eq!(next_token, Some("next123".to_string()));
}
#[test]
fn test_parse_redshift_snapshots_response() {
let response = r#"{
"snapshots": [
{
"SnapshotIdentifier": "my-cluster-snapshot-1",
"Status": "available",
"SnapshotType": "automated",
"ClusterIdentifier": "my-cluster",
"SnapshotCreateTime": "2024-01-01T00:00:00Z"
},
{
"SnapshotIdentifier": "my-cluster-snapshot-2",
"Status": "available",
"SnapshotType": "manual",
"ClusterIdentifier": "my-cluster",
"SnapshotCreateTime": "2024-01-02T00:00:00Z"
}
],
"NextToken": "next456"
}"#;
let config = ApiConfig {
response_root: Some("/snapshots".to_string()),
pagination: Some(crate::resource::protocol::PaginationConfig {
output_token: Some("/NextToken".to_string()),
..Default::default()
}),
..Default::default()
};
let handler = RestJsonProtocolHandler;
let (items, next_token) = handler.parse_items(response, &config).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0]["SnapshotIdentifier"], "my-cluster-snapshot-1");
assert_eq!(items[0]["Status"], "available");
assert_eq!(items[0]["SnapshotType"], "automated");
assert_eq!(items[0]["ClusterIdentifier"], "my-cluster");
assert_eq!(items[0]["SnapshotCreateTime"], "2024-01-01T00:00:00Z");
assert_eq!(items[1]["SnapshotIdentifier"], "my-cluster-snapshot-2");
assert_eq!(items[1]["SnapshotType"], "manual");
assert_eq!(next_token, Some("next456".to_string()));
}
}