1use std::collections::HashMap;
7
8use rustack_apigatewayv2_model::{
9 error::ApiGatewayV2Error,
10 operations::{APIGATEWAYV2_ROUTES, ApiGatewayV2Operation},
11};
12
13#[derive(Debug, Clone, Default)]
15pub struct PathParams {
16 params: HashMap<String, String>,
17}
18
19impl PathParams {
20 #[must_use]
22 pub fn get(&self, name: &str) -> Option<&str> {
23 self.params.get(name).map(String::as_str)
24 }
25
26 pub fn insert(&mut self, name: String, value: String) {
28 self.params.insert(name, value);
29 }
30
31 #[must_use]
33 pub fn len(&self) -> usize {
34 self.params.len()
35 }
36
37 #[must_use]
39 pub fn is_empty(&self) -> bool {
40 self.params.is_empty()
41 }
42
43 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
45 self.params.iter().map(|(k, v)| (k.as_str(), v.as_str()))
46 }
47}
48
49fn match_path(path: &str, pattern: &str) -> Option<PathParams> {
51 let path_segments: Vec<&str> = path.split('/').collect();
52 let pattern_segments: Vec<&str> = pattern.split('/').collect();
53
54 if path_segments.len() != pattern_segments.len() {
55 return None;
56 }
57
58 let mut params = PathParams::default();
59 for (ps, pp) in path_segments.iter().zip(pattern_segments.iter()) {
60 if pp.starts_with('{') && pp.ends_with('}') {
61 let name = &pp[1..pp.len() - 1];
62 params.insert(name.to_owned(), (*ps).to_owned());
63 } else if ps != pp {
64 return None;
65 }
66 }
67 Some(params)
68}
69
70pub fn resolve_operation(
78 method: &http::Method,
79 path: &str,
80) -> Result<(ApiGatewayV2Operation, PathParams, u16), ApiGatewayV2Error> {
81 for route in APIGATEWAYV2_ROUTES {
82 if route.method == *method {
83 if let Some(params) = match_path(path, route.path_pattern) {
84 return Ok((route.operation, params, route.success_status));
85 }
86 }
87 }
88 Err(ApiGatewayV2Error::unknown_operation(method, path))
89}
90
91#[must_use]
95pub fn percent_decode(input: &str) -> String {
96 let mut result = String::with_capacity(input.len());
97 let mut chars = input.chars();
98 while let Some(ch) = chars.next() {
99 if ch == '%' {
100 let hex: String = chars.by_ref().take(2).collect();
101 if hex.len() == 2 {
102 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
103 result.push(byte as char);
104 continue;
105 }
106 }
107 result.push('%');
109 result.push_str(&hex);
110 } else {
111 result.push(ch);
112 }
113 }
114 result
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
124 fn test_should_match_exact_path() {
125 let params = match_path("/v2/apis", "/v2/apis").expect("should match");
126 assert!(params.params.is_empty());
127 }
128
129 #[test]
130 fn test_should_match_path_with_single_param() {
131 let params = match_path("/v2/apis/abc123", "/v2/apis/{apiId}").expect("should match");
132 assert_eq!(params.get("apiId"), Some("abc123"));
133 }
134
135 #[test]
136 fn test_should_match_path_with_multiple_params() {
137 let params = match_path(
138 "/v2/apis/abc123/routes/rt-456",
139 "/v2/apis/{apiId}/routes/{routeId}",
140 )
141 .expect("should match");
142 assert_eq!(params.get("apiId"), Some("abc123"));
143 assert_eq!(params.get("routeId"), Some("rt-456"));
144 }
145
146 #[test]
147 fn test_should_match_deep_nested_path() {
148 let params = match_path(
149 "/v2/apis/abc123/routes/rt-456/routeresponses/rr-789",
150 "/v2/apis/{apiId}/routes/{routeId}/routeresponses/{routeResponseId}",
151 )
152 .expect("should match");
153 assert_eq!(params.get("apiId"), Some("abc123"));
154 assert_eq!(params.get("routeId"), Some("rt-456"));
155 assert_eq!(params.get("routeResponseId"), Some("rr-789"));
156 }
157
158 #[test]
159 fn test_should_not_match_shorter_path() {
160 assert!(match_path("/v2/apis", "/v2/apis/{apiId}").is_none());
161 }
162
163 #[test]
164 fn test_should_not_match_longer_path() {
165 assert!(match_path("/v2/apis/abc123/extra", "/v2/apis/{apiId}").is_none());
166 }
167
168 #[test]
169 fn test_should_not_match_wrong_literal_segment() {
170 assert!(match_path("/v2/wrong/abc123", "/v2/apis/{apiId}").is_none());
171 }
172
173 #[test]
176 fn test_should_resolve_create_api() {
177 let (op, params, status) =
178 resolve_operation(&http::Method::POST, "/v2/apis").expect("should resolve");
179 assert_eq!(op, ApiGatewayV2Operation::CreateApi);
180 assert!(params.params.is_empty());
181 assert_eq!(status, 201);
182 }
183
184 #[test]
185 fn test_should_resolve_get_apis() {
186 let (op, _, status) =
187 resolve_operation(&http::Method::GET, "/v2/apis").expect("should resolve");
188 assert_eq!(op, ApiGatewayV2Operation::GetApis);
189 assert_eq!(status, 200);
190 }
191
192 #[test]
193 fn test_should_resolve_get_api() {
194 let (op, params, status) =
195 resolve_operation(&http::Method::GET, "/v2/apis/abc123").expect("should resolve");
196 assert_eq!(op, ApiGatewayV2Operation::GetApi);
197 assert_eq!(params.get("apiId"), Some("abc123"));
198 assert_eq!(status, 200);
199 }
200
201 #[test]
202 fn test_should_resolve_update_api() {
203 let (op, params, _) =
204 resolve_operation(&http::Method::PATCH, "/v2/apis/abc123").expect("should resolve");
205 assert_eq!(op, ApiGatewayV2Operation::UpdateApi);
206 assert_eq!(params.get("apiId"), Some("abc123"));
207 }
208
209 #[test]
210 fn test_should_resolve_delete_api() {
211 let (op, _, status) =
212 resolve_operation(&http::Method::DELETE, "/v2/apis/abc123").expect("should resolve");
213 assert_eq!(op, ApiGatewayV2Operation::DeleteApi);
214 assert_eq!(status, 204);
215 }
216
217 #[test]
218 fn test_should_resolve_create_route() {
219 let (op, params, status) = resolve_operation(&http::Method::POST, "/v2/apis/abc123/routes")
220 .expect("should resolve");
221 assert_eq!(op, ApiGatewayV2Operation::CreateRoute);
222 assert_eq!(params.get("apiId"), Some("abc123"));
223 assert_eq!(status, 201);
224 }
225
226 #[test]
227 fn test_should_resolve_get_route() {
228 let (op, params, _) =
229 resolve_operation(&http::Method::GET, "/v2/apis/abc123/routes/rt-456")
230 .expect("should resolve");
231 assert_eq!(op, ApiGatewayV2Operation::GetRoute);
232 assert_eq!(params.get("apiId"), Some("abc123"));
233 assert_eq!(params.get("routeId"), Some("rt-456"));
234 }
235
236 #[test]
237 fn test_should_resolve_delete_route() {
238 let (op, _, status) =
239 resolve_operation(&http::Method::DELETE, "/v2/apis/abc123/routes/rt-456")
240 .expect("should resolve");
241 assert_eq!(op, ApiGatewayV2Operation::DeleteRoute);
242 assert_eq!(status, 204);
243 }
244
245 #[test]
246 fn test_should_resolve_create_integration() {
247 let (op, _, status) =
248 resolve_operation(&http::Method::POST, "/v2/apis/abc123/integrations")
249 .expect("should resolve");
250 assert_eq!(op, ApiGatewayV2Operation::CreateIntegration);
251 assert_eq!(status, 201);
252 }
253
254 #[test]
255 fn test_should_resolve_get_integration() {
256 let (op, params, _) =
257 resolve_operation(&http::Method::GET, "/v2/apis/abc123/integrations/int-789")
258 .expect("should resolve");
259 assert_eq!(op, ApiGatewayV2Operation::GetIntegration);
260 assert_eq!(params.get("integrationId"), Some("int-789"));
261 }
262
263 #[test]
264 fn test_should_resolve_create_stage() {
265 let (op, _, status) = resolve_operation(&http::Method::POST, "/v2/apis/abc123/stages")
266 .expect("should resolve");
267 assert_eq!(op, ApiGatewayV2Operation::CreateStage);
268 assert_eq!(status, 201);
269 }
270
271 #[test]
272 fn test_should_resolve_get_stage() {
273 let (op, params, _) = resolve_operation(&http::Method::GET, "/v2/apis/abc123/stages/prod")
274 .expect("should resolve");
275 assert_eq!(op, ApiGatewayV2Operation::GetStage);
276 assert_eq!(params.get("stageName"), Some("prod"));
277 }
278
279 #[test]
280 fn test_should_resolve_create_deployment() {
281 let (op, _, status) = resolve_operation(&http::Method::POST, "/v2/apis/abc123/deployments")
282 .expect("should resolve");
283 assert_eq!(op, ApiGatewayV2Operation::CreateDeployment);
284 assert_eq!(status, 201);
285 }
286
287 #[test]
288 fn test_should_resolve_get_route_response() {
289 let (op, params, status) = resolve_operation(
290 &http::Method::GET,
291 "/v2/apis/abc123/routes/rt-456/routeresponses/rr-789",
292 )
293 .expect("should resolve");
294 assert_eq!(op, ApiGatewayV2Operation::GetRouteResponse);
295 assert_eq!(params.get("apiId"), Some("abc123"));
296 assert_eq!(params.get("routeId"), Some("rt-456"));
297 assert_eq!(params.get("routeResponseId"), Some("rr-789"));
298 assert_eq!(status, 200);
299 }
300
301 #[test]
302 fn test_should_resolve_create_route_response() {
303 let (op, _, status) = resolve_operation(
304 &http::Method::POST,
305 "/v2/apis/abc123/routes/rt-456/routeresponses",
306 )
307 .expect("should resolve");
308 assert_eq!(op, ApiGatewayV2Operation::CreateRouteResponse);
309 assert_eq!(status, 201);
310 }
311
312 #[test]
313 fn test_should_resolve_get_model_template() {
314 let (op, params, _) =
315 resolve_operation(&http::Method::GET, "/v2/apis/abc123/models/mod-1/template")
316 .expect("should resolve");
317 assert_eq!(op, ApiGatewayV2Operation::GetModelTemplate);
318 assert_eq!(params.get("modelId"), Some("mod-1"));
319 }
320
321 #[test]
322 fn test_should_resolve_create_authorizer() {
323 let (op, _, status) = resolve_operation(&http::Method::POST, "/v2/apis/abc123/authorizers")
324 .expect("should resolve");
325 assert_eq!(op, ApiGatewayV2Operation::CreateAuthorizer);
326 assert_eq!(status, 201);
327 }
328
329 #[test]
330 fn test_should_resolve_create_domain_name() {
331 let (op, _, status) =
332 resolve_operation(&http::Method::POST, "/v2/domainnames").expect("should resolve");
333 assert_eq!(op, ApiGatewayV2Operation::CreateDomainName);
334 assert_eq!(status, 201);
335 }
336
337 #[test]
338 fn test_should_resolve_get_domain_name() {
339 let (op, params, _) = resolve_operation(&http::Method::GET, "/v2/domainnames/example.com")
340 .expect("should resolve");
341 assert_eq!(op, ApiGatewayV2Operation::GetDomainName);
342 assert_eq!(params.get("domainName"), Some("example.com"));
343 }
344
345 #[test]
346 fn test_should_resolve_create_vpc_link() {
347 let (op, _, status) =
348 resolve_operation(&http::Method::POST, "/v2/vpclinks").expect("should resolve");
349 assert_eq!(op, ApiGatewayV2Operation::CreateVpcLink);
350 assert_eq!(status, 201);
351 }
352
353 #[test]
354 fn test_should_resolve_delete_vpc_link() {
355 let (op, _, status) =
356 resolve_operation(&http::Method::DELETE, "/v2/vpclinks/vpc-1").expect("should resolve");
357 assert_eq!(op, ApiGatewayV2Operation::DeleteVpcLink);
358 assert_eq!(status, 202);
359 }
360
361 #[test]
362 fn test_should_resolve_tag_resource() {
363 let (op, params, status) =
364 resolve_operation(&http::Method::POST, "/v2/tags/some-arn").expect("should resolve");
365 assert_eq!(op, ApiGatewayV2Operation::TagResource);
366 assert_eq!(params.get("resource-arn"), Some("some-arn"));
367 assert_eq!(status, 201);
368 }
369
370 #[test]
371 fn test_should_resolve_get_tags() {
372 let (op, _, _) =
373 resolve_operation(&http::Method::GET, "/v2/tags/some-arn").expect("should resolve");
374 assert_eq!(op, ApiGatewayV2Operation::GetTags);
375 }
376
377 #[test]
378 fn test_should_resolve_untag_resource() {
379 let (op, _, status) =
380 resolve_operation(&http::Method::DELETE, "/v2/tags/some-arn").expect("should resolve");
381 assert_eq!(op, ApiGatewayV2Operation::UntagResource);
382 assert_eq!(status, 204);
383 }
384
385 #[test]
386 fn test_should_resolve_get_api_mapping() {
387 let (op, params, _) = resolve_operation(
388 &http::Method::GET,
389 "/v2/domainnames/example.com/apimappings/map-1",
390 )
391 .expect("should resolve");
392 assert_eq!(op, ApiGatewayV2Operation::GetApiMapping);
393 assert_eq!(params.get("domainName"), Some("example.com"));
394 assert_eq!(params.get("apiMappingId"), Some("map-1"));
395 }
396
397 #[test]
398 fn test_should_resolve_create_api_mapping() {
399 let (op, _, status) = resolve_operation(
400 &http::Method::POST,
401 "/v2/domainnames/example.com/apimappings",
402 )
403 .expect("should resolve");
404 assert_eq!(op, ApiGatewayV2Operation::CreateApiMapping);
405 assert_eq!(status, 201);
406 }
407
408 #[test]
409 fn test_should_error_on_unknown_route() {
410 let err =
411 resolve_operation(&http::Method::GET, "/v2/nonexistent").expect_err("should error");
412 assert_eq!(
413 err.code,
414 rustack_apigatewayv2_model::error::ApiGatewayV2ErrorCode::UnknownOperation
415 );
416 }
417
418 #[test]
419 fn test_should_error_on_wrong_method() {
420 let err = resolve_operation(&http::Method::PATCH, "/v2/apis").expect_err("should error");
421 assert_eq!(
422 err.code,
423 rustack_apigatewayv2_model::error::ApiGatewayV2ErrorCode::UnknownOperation
424 );
425 }
426
427 #[test]
430 fn test_should_decode_percent_encoded_colons() {
431 let decoded = percent_decode("arn%3Aaws%3Aapigateway");
432 assert_eq!(decoded, "arn:aws:apigateway");
433 }
434
435 #[test]
436 fn test_should_pass_through_plain_text() {
437 let decoded = percent_decode("my-api-id");
438 assert_eq!(decoded, "my-api-id");
439 }
440
441 #[test]
442 fn test_should_handle_malformed_percent_encoding() {
443 let decoded = percent_decode("bad%ZZstuff");
444 assert_eq!(decoded, "bad%ZZstuff");
445 }
446}