1use std::collections::HashMap;
9
10use http::StatusCode;
11
12use fakecloud_core::service::AwsServiceError;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct Filter {
18 pub name: String,
19 pub values: Vec<String>,
20}
21
22pub fn gen_id(prefix: &str) -> String {
25 let hex = uuid::Uuid::new_v4().simple().to_string();
26 format!("{prefix}-{}", &hex[..17])
27}
28
29pub fn invalid_parameter_value(message: impl Into<String>) -> AwsServiceError {
31 AwsServiceError::aws_error(
32 StatusCode::BAD_REQUEST,
33 "InvalidParameterValue",
34 message.into(),
35 )
36}
37
38pub fn missing_parameter(name: &str) -> AwsServiceError {
40 AwsServiceError::aws_error(
41 StatusCode::BAD_REQUEST,
42 "MissingParameter",
43 format!("The request must contain the parameter {name}"),
44 )
45}
46
47pub fn not_found(code: &str, id: &str) -> AwsServiceError {
49 AwsServiceError::aws_error(
50 StatusCode::BAD_REQUEST,
51 code,
52 format!("The ID '{id}' does not exist"),
53 )
54}
55
56pub fn instance_not_found(id: &str) -> AwsServiceError {
59 AwsServiceError::aws_error(
60 StatusCode::BAD_REQUEST,
61 "InvalidInstanceID.NotFound",
62 format!("The instance ID '{id}' does not exist"),
63 )
64}
65
66pub fn instance_limit_exceeded(message: impl Into<String>) -> AwsServiceError {
70 AwsServiceError::aws_error(
71 StatusCode::BAD_REQUEST,
72 "InstanceLimitExceeded",
73 message.into(),
74 )
75}
76
77pub fn incorrect_instance_state(id: &str, current: &str) -> AwsServiceError {
80 AwsServiceError::aws_error(
81 StatusCode::BAD_REQUEST,
82 "IncorrectInstanceState",
83 format!("The instance '{id}' is not in a state from which it can be modified (current state: {current})"),
84 )
85}
86
87pub fn filter_value_matches(pattern: &str, candidate: &str) -> bool {
91 if !pattern.contains('*') && !pattern.contains('?') {
92 return pattern == candidate;
93 }
94 glob_match(pattern.as_bytes(), candidate.as_bytes())
95}
96
97fn glob_match(pat: &[u8], text: &[u8]) -> bool {
99 let (mut p, mut t) = (0usize, 0usize);
100 let (mut star_p, mut star_t): (Option<usize>, usize) = (None, 0);
101 while t < text.len() {
102 if p < pat.len() && (pat[p] == b'?' || pat[p] == text[t]) {
103 p += 1;
104 t += 1;
105 } else if p < pat.len() && pat[p] == b'*' {
106 star_p = Some(p);
107 star_t = t;
108 p += 1;
109 } else if let Some(sp) = star_p {
110 p = sp + 1;
111 star_t += 1;
112 t = star_t;
113 } else {
114 return false;
115 }
116 }
117 while p < pat.len() && pat[p] == b'*' {
118 p += 1;
119 }
120 p == pat.len()
121}
122
123pub fn paginate<T: Clone>(
128 items: &[T],
129 next_token: Option<&str>,
130 max_results: Option<usize>,
131) -> (Vec<T>, Option<String>) {
132 let start = next_token
133 .and_then(|t| t.parse::<usize>().ok())
134 .unwrap_or(0);
135 let start = start.min(items.len());
136 let end = match max_results {
137 Some(n) => (start + n).min(items.len()),
138 None => items.len(),
139 };
140 let page = items[start..end].to_vec();
141 let token = if end < items.len() {
142 Some(end.to_string())
143 } else {
144 None
145 };
146 (page, token)
147}
148
149pub fn require(params: &HashMap<String, String>, key: &str) -> Result<String, AwsServiceError> {
153 params
154 .get(key)
155 .filter(|v| !v.is_empty())
156 .cloned()
157 .ok_or_else(|| missing_parameter(key))
158}
159
160pub fn require_struct(
165 params: &HashMap<String, String>,
166 prefix: &str,
167) -> Result<(), AwsServiceError> {
168 let pat = format!("{prefix}.");
169 if params.keys().any(|k| k.starts_with(&pat)) {
170 Ok(())
171 } else {
172 Err(missing_parameter(prefix))
173 }
174}
175
176pub fn validate_enum(
180 params: &HashMap<String, String>,
181 key: &str,
182 allowed: &[&str],
183) -> Result<(), AwsServiceError> {
184 if let Some(v) = params.get(key).filter(|v| !v.is_empty()) {
185 if !allowed.contains(&v.as_str()) {
186 return Err(invalid_parameter_value(format!(
187 "Invalid value '{v}' for {key}"
188 )));
189 }
190 }
191 Ok(())
192}
193
194pub fn validate_max_results(
198 params: &HashMap<String, String>,
199 min: i64,
200 max: i64,
201) -> Result<(), AwsServiceError> {
202 if let Some(v) = params.get("MaxResults").filter(|v| !v.is_empty()) {
203 if let Ok(n) = v.parse::<i64>() {
204 if n < min || n > max {
205 return Err(invalid_parameter_value(format!(
206 "MaxResults must be between {min} and {max}"
207 )));
208 }
209 }
210 }
211 Ok(())
212}
213
214pub fn validate_int_range(
218 params: &HashMap<String, String>,
219 key: &str,
220 min: i64,
221 max: i64,
222) -> Result<(), AwsServiceError> {
223 if let Some(v) = params.get(key).filter(|v| !v.is_empty()) {
224 if let Ok(n) = v.parse::<i64>() {
225 if n < min || n > max {
226 return Err(invalid_parameter_value(format!(
227 "{key} must be between {min} and {max}"
228 )));
229 }
230 }
231 }
232 Ok(())
233}
234
235pub fn validate_length(
239 params: &HashMap<String, String>,
240 key: &str,
241 min: usize,
242 max: usize,
243) -> Result<(), AwsServiceError> {
244 if let Some(v) = params.get(key) {
245 let n = v.chars().count();
246 if n < min || n > max {
247 return Err(invalid_parameter_value(format!(
248 "{key} length must be between {min} and {max}"
249 )));
250 }
251 }
252 Ok(())
253}
254
255pub fn indexed_list(params: &HashMap<String, String>, prefix: &str) -> Vec<String> {
261 let mut out = Vec::new();
262 let mut i = 1usize;
263 loop {
264 let key = format!("{prefix}.{i}");
265 match params.get(&key) {
266 Some(v) if !v.is_empty() => out.push(v.clone()),
267 _ => break,
268 }
269 i += 1;
270 }
271 out
272}
273
274pub fn parse_filters(params: &HashMap<String, String>) -> Vec<Filter> {
276 let mut out = Vec::new();
277 let mut i = 1usize;
278 loop {
279 let name_key = format!("Filter.{i}.Name");
280 let Some(name) = params.get(&name_key).filter(|v| !v.is_empty()) else {
281 break;
282 };
283 let values = indexed_list(params, &format!("Filter.{i}.Value"));
284 out.push(Filter {
285 name: name.clone(),
286 values,
287 });
288 i += 1;
289 }
290 out
291}
292
293pub fn parse_tag_pairs(
302 params: &HashMap<String, String>,
303 prefix: &str,
304) -> Vec<(String, Option<String>)> {
305 let mut out = Vec::new();
306 let mut i = 1usize;
307 loop {
308 let key_param = format!("{prefix}.{i}.Key");
309 let Some(key) = params.get(&key_param).filter(|v| !v.is_empty()) else {
310 break;
311 };
312 let value = params.get(&format!("{prefix}.{i}.Value")).cloned();
313 out.push((key.clone(), value));
314 i += 1;
315 }
316 out
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 fn p(pairs: &[(&str, &str)]) -> HashMap<String, String> {
324 pairs
325 .iter()
326 .map(|(k, v)| (k.to_string(), v.to_string()))
327 .collect()
328 }
329
330 #[test]
331 fn indexed_list_collects_contiguous_then_stops() {
332 let params = p(&[("ResourceId.1", "vpc-1"), ("ResourceId.2", "vpc-2")]);
333 assert_eq!(indexed_list(¶ms, "ResourceId"), vec!["vpc-1", "vpc-2"]);
334 }
335
336 #[test]
337 fn indexed_list_stops_at_gap() {
338 let params = p(&[("ResourceId.1", "vpc-1"), ("ResourceId.3", "vpc-3")]);
339 assert_eq!(indexed_list(¶ms, "ResourceId"), vec!["vpc-1"]);
340 }
341
342 #[test]
343 fn parse_filters_groups_name_and_values() {
344 let params = p(&[
345 ("Filter.1.Name", "resource-id"),
346 ("Filter.1.Value.1", "vpc-1"),
347 ("Filter.1.Value.2", "vpc-2"),
348 ("Filter.2.Name", "key"),
349 ("Filter.2.Value.1", "Name"),
350 ]);
351 let filters = parse_filters(¶ms);
352 assert_eq!(filters.len(), 2);
353 assert_eq!(
354 filters[0],
355 Filter {
356 name: "resource-id".into(),
357 values: vec!["vpc-1".into(), "vpc-2".into()]
358 }
359 );
360 assert_eq!(
361 filters[1],
362 Filter {
363 name: "key".into(),
364 values: vec!["Name".into()]
365 }
366 );
367 }
368
369 #[test]
370 fn parse_tag_pairs_handles_optional_value() {
371 let params = p(&[
372 ("Tag.1.Key", "Name"),
373 ("Tag.1.Value", "web"),
374 ("Tag.2.Key", "env"),
375 ]);
376 let tags = parse_tag_pairs(¶ms, "Tag");
377 assert_eq!(
378 tags,
379 vec![("Name".into(), Some("web".into())), ("env".into(), None)]
380 );
381 }
382
383 #[test]
384 fn filter_wildcards() {
385 assert!(filter_value_matches("web", "web"));
386 assert!(!filter_value_matches("web", "web1"));
387 assert!(filter_value_matches("web*", "web-prod"));
388 assert!(filter_value_matches("*prod", "web-prod"));
389 assert!(filter_value_matches("web*prod", "web-staging-prod"));
390 assert!(filter_value_matches("we?", "web"));
391 assert!(!filter_value_matches("we?", "web1"));
392 assert!(filter_value_matches("*", "anything"));
393 assert!(!filter_value_matches("web?", "web"));
394 }
395
396 #[test]
397 fn paginate_pages_and_round_trips_token() {
398 let items: Vec<i32> = (0..10).collect();
399 let (page, token) = paginate(&items, None, Some(4));
400 assert_eq!(page, vec![0, 1, 2, 3]);
401 assert_eq!(token.as_deref(), Some("4"));
402 let (page2, token2) = paginate(&items, token.as_deref(), Some(4));
403 assert_eq!(page2, vec![4, 5, 6, 7]);
404 assert_eq!(token2.as_deref(), Some("8"));
405 let (page3, token3) = paginate(&items, token2.as_deref(), Some(4));
406 assert_eq!(page3, vec![8, 9]);
407 assert_eq!(token3, None);
408 }
409
410 #[test]
411 fn paginate_no_max_returns_all() {
412 let items: Vec<i32> = (0..3).collect();
413 let (page, token) = paginate(&items, None, None);
414 assert_eq!(page, items);
415 assert_eq!(token, None);
416 }
417
418 #[test]
419 fn parse_tag_pairs_distinguishes_empty_value_from_absent() {
420 let params = p(&[("Tag.1.Key", "a"), ("Tag.1.Value", ""), ("Tag.2.Key", "b")]);
424 let tags = parse_tag_pairs(¶ms, "Tag");
425 assert_eq!(
426 tags,
427 vec![("a".into(), Some("".into())), ("b".into(), None)]
428 );
429 }
430}