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 instance_not_found(id: &str) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidInstanceID.NotFound",
format!("The instance ID '{id}' does not exist"),
)
}
pub fn instance_limit_exceeded(message: impl Into<String>) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InstanceLimitExceeded",
message.into(),
)
}
pub fn incorrect_instance_state(id: &str, current: &str) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"IncorrectInstanceState",
format!("The instance '{id}' is not in a state from which it can be modified (current state: {current})"),
)
}
pub fn filter_value_matches(pattern: &str, candidate: &str) -> bool {
if !pattern.contains('*') && !pattern.contains('?') {
return pattern == candidate;
}
glob_match(pattern.as_bytes(), candidate.as_bytes())
}
fn glob_match(pat: &[u8], text: &[u8]) -> bool {
let (mut p, mut t) = (0usize, 0usize);
let (mut star_p, mut star_t): (Option<usize>, usize) = (None, 0);
while t < text.len() {
if p < pat.len() && (pat[p] == b'?' || pat[p] == text[t]) {
p += 1;
t += 1;
} else if p < pat.len() && pat[p] == b'*' {
star_p = Some(p);
star_t = t;
p += 1;
} else if let Some(sp) = star_p {
p = sp + 1;
star_t += 1;
t = star_t;
} else {
return false;
}
}
while p < pat.len() && pat[p] == b'*' {
p += 1;
}
p == pat.len()
}
pub fn paginate<T: Clone>(
items: &[T],
next_token: Option<&str>,
max_results: Option<usize>,
) -> (Vec<T>, Option<String>) {
let start = next_token
.and_then(|t| t.parse::<usize>().ok())
.unwrap_or(0);
let start = start.min(items.len());
let end = match max_results {
Some(n) => (start + n).min(items.len()),
None => items.len(),
};
let page = items[start..end].to_vec();
let token = if end < items.len() {
Some(end.to_string())
} else {
None
};
(page, token)
}
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 filter_wildcards() {
assert!(filter_value_matches("web", "web"));
assert!(!filter_value_matches("web", "web1"));
assert!(filter_value_matches("web*", "web-prod"));
assert!(filter_value_matches("*prod", "web-prod"));
assert!(filter_value_matches("web*prod", "web-staging-prod"));
assert!(filter_value_matches("we?", "web"));
assert!(!filter_value_matches("we?", "web1"));
assert!(filter_value_matches("*", "anything"));
assert!(!filter_value_matches("web?", "web"));
}
#[test]
fn paginate_pages_and_round_trips_token() {
let items: Vec<i32> = (0..10).collect();
let (page, token) = paginate(&items, None, Some(4));
assert_eq!(page, vec![0, 1, 2, 3]);
assert_eq!(token.as_deref(), Some("4"));
let (page2, token2) = paginate(&items, token.as_deref(), Some(4));
assert_eq!(page2, vec![4, 5, 6, 7]);
assert_eq!(token2.as_deref(), Some("8"));
let (page3, token3) = paginate(&items, token2.as_deref(), Some(4));
assert_eq!(page3, vec![8, 9]);
assert_eq!(token3, None);
}
#[test]
fn paginate_no_max_returns_all() {
let items: Vec<i32> = (0..3).collect();
let (page, token) = paginate(&items, None, None);
assert_eq!(page, items);
assert_eq!(token, 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)]
);
}
}