use std::collections::HashMap;
use http::{HeaderMap, StatusCode};
use serde_json::{Map, Value};
use fakecloud_core::service::AwsServiceError;
use crate::generated::{OpMeta, Src, K};
pub fn validation_error_code(meta: &OpMeta) -> &'static str {
if meta.errors.contains(&"InvalidRequestException") {
"InvalidRequestException"
} else if meta.errors.contains(&"ValidationException") {
"ValidationException"
} else if let Some(first) = meta.errors.first() {
first
} else {
"InvalidRequestException"
}
}
pub fn invalid(meta: &OpMeta, msg: impl Into<String>) -> AwsServiceError {
AwsServiceError::aws_error(StatusCode::BAD_REQUEST, validation_error_code(meta), msg)
}
pub struct Inputs<'a> {
pub labels: &'a HashMap<String, String>,
pub query: &'a [(String, String)],
pub headers: &'a HeaderMap,
pub body: &'a Map<String, Value>,
}
impl Inputs<'_> {
fn query_get(&self, key: &str) -> Option<&str> {
self.query
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
fn header_get(&self, key: &str) -> Option<&str> {
self.headers.get(key).and_then(|v| v.to_str().ok())
}
}
pub fn validate(meta: &OpMeta, inputs: &Inputs) -> Result<(), AwsServiceError> {
if meta.req_payload && inputs.body.is_empty() {
return Err(invalid(meta, "Missing required request payload."));
}
for rule in meta.rules {
let string_val: Option<String> = match rule.src {
Src::Label => inputs.labels.get(rule.wire).cloned(),
Src::Query => inputs.query_get(rule.wire).map(str::to_string),
Src::Header => inputs.header_get(rule.wire).map(str::to_string),
Src::Body => None,
};
let body_val: Option<&Value> = match rule.src {
Src::Body => inputs.body.get(rule.wire),
_ => None,
};
let present = string_val.is_some() || body_val.is_some();
if rule.req && !present {
return Err(invalid(
meta,
format!("Missing required request parameter: {}", rule.wire),
));
}
if !present {
continue;
}
if matches!(rule.kind, K::Str) {
let s: Option<String> = match &string_val {
Some(s) => Some(s.clone()),
None => match body_val {
Some(Value::String(s)) => Some(s.clone()),
Some(Value::Null) => None,
Some(_other) => {
return Err(invalid(
meta,
format!("Member {} must be a string.", rule.wire),
));
}
None => None,
},
};
if let Some(s) = s {
let len = s.chars().count() as u64;
if let Some(min) = rule.min_len {
if len < min {
return Err(invalid(
meta,
format!(
"Member {} is shorter than the minimum length {min}.",
rule.wire
),
));
}
}
if let Some(max) = rule.max_len {
if len > max {
return Err(invalid(
meta,
format!(
"Member {} is longer than the maximum length {max}.",
rule.wire
),
));
}
}
if !rule.enums.is_empty() && !rule.enums.contains(&s.as_str()) {
return Err(invalid(
meta,
format!("Member {} has an invalid enum value.", rule.wire),
));
}
}
}
if matches!(rule.kind, K::Int) && (rule.min_val.is_some() || rule.max_val.is_some()) {
let n: Option<i64> = match &string_val {
Some(s) => s.parse::<i64>().ok(),
None => body_val.and_then(Value::as_i64),
};
if let Some(n) = n {
if let Some(min) = rule.min_val {
if n < min {
return Err(invalid(
meta,
format!("Member {} is below the minimum {min}.", rule.wire),
));
}
}
if let Some(max) = rule.max_val {
if n > max {
return Err(invalid(
meta,
format!("Member {} is above the maximum {max}.", rule.wire),
));
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::OPS;
fn meta(op: &str) -> &'static OpMeta {
OPS.iter().find(|m| m.op == op).unwrap()
}
fn inputs<'a>(
labels: &'a HashMap<String, String>,
query: &'a [(String, String)],
headers: &'a HeaderMap,
body: &'a Map<String, Value>,
) -> Inputs<'a> {
Inputs {
labels,
query,
headers,
body,
}
}
#[test]
fn rejects_missing_required_body_member() {
let m = meta("AttachPolicy");
let mut labels = HashMap::new();
labels.insert("policyName".to_string(), "p".to_string());
let body = Map::new();
let hm = HeaderMap::new();
let inp = inputs(&labels, &[], &hm, &body);
assert!(validate(m, &inp).is_err());
}
#[test]
fn accepts_valid_body() {
let m = meta("AttachPolicy");
let mut labels = HashMap::new();
labels.insert("policyName".to_string(), "p".to_string());
let mut body = Map::new();
body.insert("target".to_string(), Value::String("arn:x".to_string()));
let hm = HeaderMap::new();
let inp = inputs(&labels, &[], &hm, &body);
assert!(validate(m, &inp).is_ok());
}
#[test]
fn rejects_too_long_label() {
let m = meta("DescribeCertificate");
let mut labels = HashMap::new();
labels.insert("certificateId".to_string(), "a".repeat(65));
let body = Map::new();
let hm = HeaderMap::new();
let inp = inputs(&labels, &[], &hm, &body);
assert!(validate(m, &inp).is_err());
}
#[test]
fn accepts_boundary_length() {
let m = meta("DescribeCertificate");
let mut labels = HashMap::new();
labels.insert("certificateId".to_string(), "a".repeat(64));
let body = Map::new();
let hm = HeaderMap::new();
let inp = inputs(&labels, &[], &hm, &body);
assert!(validate(m, &inp).is_ok());
}
}