1use std::collections::HashMap;
17
18use http::{HeaderMap, StatusCode};
19use serde_json::{Map, Value};
20
21use fakecloud_core::service::AwsServiceError;
22
23use crate::generated::{OpMeta, Src, K};
24
25pub fn validation_error_code(meta: &OpMeta) -> &'static str {
29 if meta.errors.contains(&"InvalidRequestException") {
30 "InvalidRequestException"
31 } else if meta.errors.contains(&"ValidationException") {
32 "ValidationException"
33 } else if let Some(first) = meta.errors.first() {
34 first
35 } else {
36 "InvalidRequestException"
37 }
38}
39
40pub fn invalid(meta: &OpMeta, msg: impl Into<String>) -> AwsServiceError {
41 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, validation_error_code(meta), msg)
42}
43
44pub struct Inputs<'a> {
46 pub labels: &'a HashMap<String, String>,
47 pub query: &'a [(String, String)],
48 pub headers: &'a HeaderMap,
49 pub body: &'a Map<String, Value>,
50}
51
52impl Inputs<'_> {
53 fn query_get(&self, key: &str) -> Option<&str> {
54 self.query
55 .iter()
56 .find(|(k, _)| k == key)
57 .map(|(_, v)| v.as_str())
58 }
59 fn header_get(&self, key: &str) -> Option<&str> {
60 self.headers.get(key).and_then(|v| v.to_str().ok())
61 }
62}
63
64pub fn validate(meta: &OpMeta, inputs: &Inputs) -> Result<(), AwsServiceError> {
66 if meta.req_payload && inputs.body.is_empty() {
70 return Err(invalid(meta, "Missing required request payload."));
71 }
72
73 for rule in meta.rules {
74 let string_val: Option<String> = match rule.src {
77 Src::Label => inputs.labels.get(rule.wire).cloned(),
78 Src::Query => inputs.query_get(rule.wire).map(str::to_string),
79 Src::Header => inputs.header_get(rule.wire).map(str::to_string),
80 Src::Body => None,
81 };
82 let body_val: Option<&Value> = match rule.src {
83 Src::Body => inputs.body.get(rule.wire),
84 _ => None,
85 };
86 let present = string_val.is_some() || body_val.is_some();
87
88 if rule.req && !present {
89 return Err(invalid(
90 meta,
91 format!("Missing required request parameter: {}", rule.wire),
92 ));
93 }
94 if !present {
95 continue;
96 }
97
98 if matches!(rule.kind, K::Str) {
103 let s: Option<String> = match &string_val {
104 Some(s) => Some(s.clone()),
105 None => match body_val {
106 Some(Value::String(s)) => Some(s.clone()),
107 Some(Value::Null) => None,
108 Some(_other) => {
109 return Err(invalid(
110 meta,
111 format!("Member {} must be a string.", rule.wire),
112 ));
113 }
114 None => None,
115 },
116 };
117 if let Some(s) = s {
118 let len = s.chars().count() as u64;
119 if let Some(min) = rule.min_len {
120 if len < min {
121 return Err(invalid(
122 meta,
123 format!(
124 "Member {} is shorter than the minimum length {min}.",
125 rule.wire
126 ),
127 ));
128 }
129 }
130 if let Some(max) = rule.max_len {
131 if len > max {
132 return Err(invalid(
133 meta,
134 format!(
135 "Member {} is longer than the maximum length {max}.",
136 rule.wire
137 ),
138 ));
139 }
140 }
141 if !rule.enums.is_empty() && !rule.enums.contains(&s.as_str()) {
142 return Err(invalid(
143 meta,
144 format!("Member {} has an invalid enum value.", rule.wire),
145 ));
146 }
147 }
148 }
149
150 if matches!(rule.kind, K::Int) && (rule.min_val.is_some() || rule.max_val.is_some()) {
152 let n: Option<i64> = match &string_val {
153 Some(s) => s.parse::<i64>().ok(),
154 None => body_val.and_then(Value::as_i64),
155 };
156 if let Some(n) = n {
157 if let Some(min) = rule.min_val {
158 if n < min {
159 return Err(invalid(
160 meta,
161 format!("Member {} is below the minimum {min}.", rule.wire),
162 ));
163 }
164 }
165 if let Some(max) = rule.max_val {
166 if n > max {
167 return Err(invalid(
168 meta,
169 format!("Member {} is above the maximum {max}.", rule.wire),
170 ));
171 }
172 }
173 }
174 }
175 }
176 Ok(())
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::generated::OPS;
183
184 fn meta(op: &str) -> &'static OpMeta {
185 OPS.iter().find(|m| m.op == op).unwrap()
186 }
187 fn inputs<'a>(
188 labels: &'a HashMap<String, String>,
189 query: &'a [(String, String)],
190 headers: &'a HeaderMap,
191 body: &'a Map<String, Value>,
192 ) -> Inputs<'a> {
193 Inputs {
194 labels,
195 query,
196 headers,
197 body,
198 }
199 }
200
201 #[test]
202 fn rejects_missing_required_body_member() {
203 let m = meta("AttachPolicy");
205 let mut labels = HashMap::new();
206 labels.insert("policyName".to_string(), "p".to_string());
207 let body = Map::new();
208 let hm = HeaderMap::new();
209 let inp = inputs(&labels, &[], &hm, &body);
210 assert!(validate(m, &inp).is_err());
211 }
212
213 #[test]
214 fn accepts_valid_body() {
215 let m = meta("AttachPolicy");
216 let mut labels = HashMap::new();
217 labels.insert("policyName".to_string(), "p".to_string());
218 let mut body = Map::new();
219 body.insert("target".to_string(), Value::String("arn:x".to_string()));
220 let hm = HeaderMap::new();
221 let inp = inputs(&labels, &[], &hm, &body);
222 assert!(validate(m, &inp).is_ok());
223 }
224
225 #[test]
226 fn rejects_too_long_label() {
227 let m = meta("DescribeCertificate");
229 let mut labels = HashMap::new();
230 labels.insert("certificateId".to_string(), "a".repeat(65));
231 let body = Map::new();
232 let hm = HeaderMap::new();
233 let inp = inputs(&labels, &[], &hm, &body);
234 assert!(validate(m, &inp).is_err());
235 }
236
237 #[test]
238 fn accepts_boundary_length() {
239 let m = meta("DescribeCertificate");
240 let mut labels = HashMap::new();
241 labels.insert("certificateId".to_string(), "a".repeat(64));
242 let body = Map::new();
243 let hm = HeaderMap::new();
244 let inp = inputs(&labels, &[], &hm, &body);
245 assert!(validate(m, &inp).is_ok());
246 }
247}