use std::collections::HashMap;
use http::StatusCode;
use fakecloud_core::service::AwsServiceError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Filter {
pub name: String,
pub values: Vec<String>,
}
pub fn gen_id(prefix: &str) -> String {
let hex = uuid::Uuid::new_v4().simple().to_string();
format!("{prefix}-{}", &hex[..17])
}
pub fn invalid_parameter_value(message: impl Into<String>) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidParameterValue",
message.into(),
)
}
pub fn missing_parameter(name: &str) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"MissingParameter",
format!("The request must contain the parameter {name}"),
)
}
pub fn not_found(code: &str, id: &str) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
code,
format!("The ID '{id}' does not exist"),
)
}
pub fn require(params: &HashMap<String, String>, key: &str) -> Result<String, AwsServiceError> {
params
.get(key)
.filter(|v| !v.is_empty())
.cloned()
.ok_or_else(|| missing_parameter(key))
}
pub fn require_struct(
params: &HashMap<String, String>,
prefix: &str,
) -> Result<(), AwsServiceError> {
let pat = format!("{prefix}.");
if params.keys().any(|k| k.starts_with(&pat)) {
Ok(())
} else {
Err(missing_parameter(prefix))
}
}
pub fn validate_enum(
params: &HashMap<String, String>,
key: &str,
allowed: &[&str],
) -> Result<(), AwsServiceError> {
if let Some(v) = params.get(key).filter(|v| !v.is_empty()) {
if !allowed.contains(&v.as_str()) {
return Err(invalid_parameter_value(format!(
"Invalid value '{v}' for {key}"
)));
}
}
Ok(())
}
pub fn validate_max_results(
params: &HashMap<String, String>,
min: i64,
max: i64,
) -> Result<(), AwsServiceError> {
if let Some(v) = params.get("MaxResults").filter(|v| !v.is_empty()) {
if let Ok(n) = v.parse::<i64>() {
if n < min || n > max {
return Err(invalid_parameter_value(format!(
"MaxResults must be between {min} and {max}"
)));
}
}
}
Ok(())
}
pub fn validate_int_range(
params: &HashMap<String, String>,
key: &str,
min: i64,
max: i64,
) -> Result<(), AwsServiceError> {
if let Some(v) = params.get(key).filter(|v| !v.is_empty()) {
if let Ok(n) = v.parse::<i64>() {
if n < min || n > max {
return Err(invalid_parameter_value(format!(
"{key} must be between {min} and {max}"
)));
}
}
}
Ok(())
}
pub fn validate_length(
params: &HashMap<String, String>,
key: &str,
min: usize,
max: usize,
) -> Result<(), AwsServiceError> {
if let Some(v) = params.get(key) {
let n = v.chars().count();
if n < min || n > max {
return Err(invalid_parameter_value(format!(
"{key} length must be between {min} and {max}"
)));
}
}
Ok(())
}
pub fn indexed_list(params: &HashMap<String, String>, prefix: &str) -> Vec<String> {
let mut out = Vec::new();
let mut i = 1usize;
loop {
let key = format!("{prefix}.{i}");
match params.get(&key) {
Some(v) if !v.is_empty() => out.push(v.clone()),
_ => break,
}
i += 1;
}
out
}
pub fn parse_filters(params: &HashMap<String, String>) -> Vec<Filter> {
let mut out = Vec::new();
let mut i = 1usize;
loop {
let name_key = format!("Filter.{i}.Name");
let Some(name) = params.get(&name_key).filter(|v| !v.is_empty()) else {
break;
};
let values = indexed_list(params, &format!("Filter.{i}.Value"));
out.push(Filter {
name: name.clone(),
values,
});
i += 1;
}
out
}
pub fn parse_tag_pairs(
params: &HashMap<String, String>,
prefix: &str,
) -> Vec<(String, Option<String>)> {
let mut out = Vec::new();
let mut i = 1usize;
loop {
let key_param = format!("{prefix}.{i}.Key");
let Some(key) = params.get(&key_param).filter(|v| !v.is_empty()) else {
break;
};
let value = params.get(&format!("{prefix}.{i}.Value")).cloned();
out.push((key.clone(), value));
i += 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn p(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn indexed_list_collects_contiguous_then_stops() {
let params = p(&[("ResourceId.1", "vpc-1"), ("ResourceId.2", "vpc-2")]);
assert_eq!(indexed_list(¶ms, "ResourceId"), vec!["vpc-1", "vpc-2"]);
}
#[test]
fn indexed_list_stops_at_gap() {
let params = p(&[("ResourceId.1", "vpc-1"), ("ResourceId.3", "vpc-3")]);
assert_eq!(indexed_list(¶ms, "ResourceId"), vec!["vpc-1"]);
}
#[test]
fn parse_filters_groups_name_and_values() {
let params = p(&[
("Filter.1.Name", "resource-id"),
("Filter.1.Value.1", "vpc-1"),
("Filter.1.Value.2", "vpc-2"),
("Filter.2.Name", "key"),
("Filter.2.Value.1", "Name"),
]);
let filters = parse_filters(¶ms);
assert_eq!(filters.len(), 2);
assert_eq!(
filters[0],
Filter {
name: "resource-id".into(),
values: vec!["vpc-1".into(), "vpc-2".into()]
}
);
assert_eq!(
filters[1],
Filter {
name: "key".into(),
values: vec!["Name".into()]
}
);
}
#[test]
fn parse_tag_pairs_handles_optional_value() {
let params = p(&[
("Tag.1.Key", "Name"),
("Tag.1.Value", "web"),
("Tag.2.Key", "env"),
]);
let tags = parse_tag_pairs(¶ms, "Tag");
assert_eq!(
tags,
vec![("Name".into(), Some("web".into())), ("env".into(), None)]
);
}
#[test]
fn parse_tag_pairs_distinguishes_empty_value_from_absent() {
let params = p(&[("Tag.1.Key", "a"), ("Tag.1.Value", ""), ("Tag.2.Key", "b")]);
let tags = parse_tag_pairs(¶ms, "Tag");
assert_eq!(
tags,
vec![("a".into(), Some("".into())), ("b".into(), None)]
);
}
}