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 require(params: &HashMap<String, String>, key: &str) -> Result<String, AwsServiceError> {
60 params
61 .get(key)
62 .filter(|v| !v.is_empty())
63 .cloned()
64 .ok_or_else(|| missing_parameter(key))
65}
66
67pub fn require_struct(
72 params: &HashMap<String, String>,
73 prefix: &str,
74) -> Result<(), AwsServiceError> {
75 let pat = format!("{prefix}.");
76 if params.keys().any(|k| k.starts_with(&pat)) {
77 Ok(())
78 } else {
79 Err(missing_parameter(prefix))
80 }
81}
82
83pub fn validate_enum(
87 params: &HashMap<String, String>,
88 key: &str,
89 allowed: &[&str],
90) -> Result<(), AwsServiceError> {
91 if let Some(v) = params.get(key).filter(|v| !v.is_empty()) {
92 if !allowed.contains(&v.as_str()) {
93 return Err(invalid_parameter_value(format!(
94 "Invalid value '{v}' for {key}"
95 )));
96 }
97 }
98 Ok(())
99}
100
101pub fn validate_max_results(
105 params: &HashMap<String, String>,
106 min: i64,
107 max: i64,
108) -> Result<(), AwsServiceError> {
109 if let Some(v) = params.get("MaxResults").filter(|v| !v.is_empty()) {
110 if let Ok(n) = v.parse::<i64>() {
111 if n < min || n > max {
112 return Err(invalid_parameter_value(format!(
113 "MaxResults must be between {min} and {max}"
114 )));
115 }
116 }
117 }
118 Ok(())
119}
120
121pub fn validate_int_range(
125 params: &HashMap<String, String>,
126 key: &str,
127 min: i64,
128 max: i64,
129) -> Result<(), AwsServiceError> {
130 if let Some(v) = params.get(key).filter(|v| !v.is_empty()) {
131 if let Ok(n) = v.parse::<i64>() {
132 if n < min || n > max {
133 return Err(invalid_parameter_value(format!(
134 "{key} must be between {min} and {max}"
135 )));
136 }
137 }
138 }
139 Ok(())
140}
141
142pub fn validate_length(
146 params: &HashMap<String, String>,
147 key: &str,
148 min: usize,
149 max: usize,
150) -> Result<(), AwsServiceError> {
151 if let Some(v) = params.get(key) {
152 let n = v.chars().count();
153 if n < min || n > max {
154 return Err(invalid_parameter_value(format!(
155 "{key} length must be between {min} and {max}"
156 )));
157 }
158 }
159 Ok(())
160}
161
162pub fn indexed_list(params: &HashMap<String, String>, prefix: &str) -> Vec<String> {
168 let mut out = Vec::new();
169 let mut i = 1usize;
170 loop {
171 let key = format!("{prefix}.{i}");
172 match params.get(&key) {
173 Some(v) if !v.is_empty() => out.push(v.clone()),
174 _ => break,
175 }
176 i += 1;
177 }
178 out
179}
180
181pub fn parse_filters(params: &HashMap<String, String>) -> Vec<Filter> {
183 let mut out = Vec::new();
184 let mut i = 1usize;
185 loop {
186 let name_key = format!("Filter.{i}.Name");
187 let Some(name) = params.get(&name_key).filter(|v| !v.is_empty()) else {
188 break;
189 };
190 let values = indexed_list(params, &format!("Filter.{i}.Value"));
191 out.push(Filter {
192 name: name.clone(),
193 values,
194 });
195 i += 1;
196 }
197 out
198}
199
200pub fn parse_tag_pairs(
209 params: &HashMap<String, String>,
210 prefix: &str,
211) -> Vec<(String, Option<String>)> {
212 let mut out = Vec::new();
213 let mut i = 1usize;
214 loop {
215 let key_param = format!("{prefix}.{i}.Key");
216 let Some(key) = params.get(&key_param).filter(|v| !v.is_empty()) else {
217 break;
218 };
219 let value = params.get(&format!("{prefix}.{i}.Value")).cloned();
220 out.push((key.clone(), value));
221 i += 1;
222 }
223 out
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn p(pairs: &[(&str, &str)]) -> HashMap<String, String> {
231 pairs
232 .iter()
233 .map(|(k, v)| (k.to_string(), v.to_string()))
234 .collect()
235 }
236
237 #[test]
238 fn indexed_list_collects_contiguous_then_stops() {
239 let params = p(&[("ResourceId.1", "vpc-1"), ("ResourceId.2", "vpc-2")]);
240 assert_eq!(indexed_list(¶ms, "ResourceId"), vec!["vpc-1", "vpc-2"]);
241 }
242
243 #[test]
244 fn indexed_list_stops_at_gap() {
245 let params = p(&[("ResourceId.1", "vpc-1"), ("ResourceId.3", "vpc-3")]);
246 assert_eq!(indexed_list(¶ms, "ResourceId"), vec!["vpc-1"]);
247 }
248
249 #[test]
250 fn parse_filters_groups_name_and_values() {
251 let params = p(&[
252 ("Filter.1.Name", "resource-id"),
253 ("Filter.1.Value.1", "vpc-1"),
254 ("Filter.1.Value.2", "vpc-2"),
255 ("Filter.2.Name", "key"),
256 ("Filter.2.Value.1", "Name"),
257 ]);
258 let filters = parse_filters(¶ms);
259 assert_eq!(filters.len(), 2);
260 assert_eq!(
261 filters[0],
262 Filter {
263 name: "resource-id".into(),
264 values: vec!["vpc-1".into(), "vpc-2".into()]
265 }
266 );
267 assert_eq!(
268 filters[1],
269 Filter {
270 name: "key".into(),
271 values: vec!["Name".into()]
272 }
273 );
274 }
275
276 #[test]
277 fn parse_tag_pairs_handles_optional_value() {
278 let params = p(&[
279 ("Tag.1.Key", "Name"),
280 ("Tag.1.Value", "web"),
281 ("Tag.2.Key", "env"),
282 ]);
283 let tags = parse_tag_pairs(¶ms, "Tag");
284 assert_eq!(
285 tags,
286 vec![("Name".into(), Some("web".into())), ("env".into(), None)]
287 );
288 }
289
290 #[test]
291 fn parse_tag_pairs_distinguishes_empty_value_from_absent() {
292 let params = p(&[("Tag.1.Key", "a"), ("Tag.1.Value", ""), ("Tag.2.Key", "b")]);
296 let tags = parse_tag_pairs(¶ms, "Tag");
297 assert_eq!(
298 tags,
299 vec![("a".into(), Some("".into())), ("b".into(), None)]
300 );
301 }
302}