Skip to main content

fakecloud_cloudfront/
router.rs

1//! HTTP method + URI to action routing for CloudFront's REST-XML API.
2//!
3//! Every CloudFront operation is keyed off `(Method, segments[..])`. The
4//! returned [`Route`] carries the operation name, any captured path
5//! parameters (e.g. distribution `Id`), and a flag for the
6//! `WithTags`-style query toggle so handlers don't have to re-parse it.
7
8use http::Method;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Route {
12    pub action: &'static str,
13    pub id: Option<String>,
14    pub second_id: Option<String>,
15    pub with_tags: bool,
16}
17
18impl Route {
19    fn just(action: &'static str) -> Self {
20        Self {
21            action,
22            id: None,
23            second_id: None,
24            with_tags: false,
25        }
26    }
27
28    fn with_id(action: &'static str, id: &str) -> Self {
29        Self {
30            action,
31            id: Some(id.to_string()),
32            second_id: None,
33            with_tags: false,
34        }
35    }
36
37    fn with_two(action: &'static str, id: &str, second: &str) -> Self {
38        Self {
39            action,
40            id: Some(id.to_string()),
41            second_id: Some(second.to_string()),
42            with_tags: false,
43        }
44    }
45
46    fn flag_with_tags(mut self) -> Self {
47        self.with_tags = true;
48        self
49    }
50}
51
52pub fn route(method: &Method, path: &str, raw_query: &str) -> Option<Route> {
53    let path = path.strip_prefix("/2020-05-31").unwrap_or(path);
54    let path = path.trim_start_matches('/');
55    // Filter out empty segments so a trailing slash (`/distribution/`, which
56    // botocore/AWS CLI < 1.40 and curl emit for a collection root) routes to the
57    // List op, not GetDistribution(id="") -> NoSuchDistribution (the #1645
58    // shape; bug-audit 2026-06-20, 1.1).
59    let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
60    let q = QueryFlags::parse(raw_query);
61    match (method, segs.as_slice()) {
62        // ─── Distributions ──────────────────────────────────────────
63        (&Method::POST, ["distribution"]) if q.with_tags => {
64            Some(Route::just("CreateDistributionWithTags").flag_with_tags())
65        }
66        (&Method::POST, ["distribution"]) => Some(Route::just("CreateDistribution")),
67        (&Method::GET, ["distribution"]) => Some(Route::just("ListDistributions")),
68        (&Method::GET, ["distribution", id]) => Some(Route::with_id("GetDistribution", id)),
69        (&Method::GET, ["distribution", id, "config"]) => {
70            Some(Route::with_id("GetDistributionConfig", id))
71        }
72        (&Method::PUT, ["distribution", id, "config"]) => {
73            Some(Route::with_id("UpdateDistribution", id))
74        }
75        (&Method::PUT, ["distribution", id, "promote-staging-config"]) => {
76            Some(Route::with_id("UpdateDistributionWithStagingConfig", id))
77        }
78        (&Method::DELETE, ["distribution", id]) => Some(Route::with_id("DeleteDistribution", id)),
79        (&Method::POST, ["distribution", id, "copy"]) => {
80            Some(Route::with_id("CopyDistribution", id))
81        }
82        (&Method::PUT, ["distribution", id, "associate-alias"]) => {
83            Some(Route::with_id("AssociateAlias", id))
84        }
85        (&Method::PUT, ["distribution", id, "associate-web-acl"]) => {
86            Some(Route::with_id("AssociateDistributionWebACL", id))
87        }
88        (&Method::PUT, ["distribution", id, "disassociate-web-acl"]) => {
89            Some(Route::with_id("DisassociateDistributionWebACL", id))
90        }
91
92        // ─── Distributions-by-X listings ────────────────────────────
93        (&Method::GET, ["distributionsByCachePolicyId", id]) => {
94            Some(Route::with_id("ListDistributionsByCachePolicyId", id))
95        }
96        (&Method::GET, ["distributionsByOriginRequestPolicyId", id]) => Some(Route::with_id(
97            "ListDistributionsByOriginRequestPolicyId",
98            id,
99        )),
100        (&Method::GET, ["distributionsByResponseHeadersPolicyId", id]) => Some(Route::with_id(
101            "ListDistributionsByResponseHeadersPolicyId",
102            id,
103        )),
104        (&Method::GET, ["distributionsByKeyGroupId", id]) => {
105            Some(Route::with_id("ListDistributionsByKeyGroup", id))
106        }
107        (&Method::GET, ["distributionsByWebACLId", id]) => {
108            Some(Route::with_id("ListDistributionsByWebACLId", id))
109        }
110        (&Method::GET, ["distributionsByVpcOriginId", id]) => {
111            Some(Route::with_id("ListDistributionsByVpcOriginId", id))
112        }
113        (&Method::GET, ["distributionsByAnycastIpListId", id]) => {
114            Some(Route::with_id("ListDistributionsByAnycastIpListId", id))
115        }
116        (&Method::GET, ["distributionsByConnectionMode", id]) => {
117            Some(Route::with_id("ListDistributionsByConnectionMode", id))
118        }
119        (&Method::GET, ["distributionsByConnectionFunction"]) => {
120            Some(Route::just("ListDistributionsByConnectionFunction"))
121        }
122        (&Method::GET, ["distributionsByOwnedResource", arn]) => {
123            Some(Route::with_id("ListDistributionsByOwnedResource", arn))
124        }
125        (&Method::GET, ["distributionsByTrustStore"]) => {
126            Some(Route::just("ListDistributionsByTrustStore"))
127        }
128        (&Method::POST, ["distributionsByRealtimeLogConfig"]) => {
129            Some(Route::just("ListDistributionsByRealtimeLogConfig"))
130        }
131        (&Method::GET, ["conflicting-alias"]) => Some(Route::just("ListConflictingAliases")),
132
133        // ─── Invalidations ──────────────────────────────────────────
134        (&Method::POST, ["distribution", dist, "invalidation"]) => {
135            Some(Route::with_id("CreateInvalidation", dist))
136        }
137        (&Method::GET, ["distribution", dist, "invalidation"]) => {
138            Some(Route::with_id("ListInvalidations", dist))
139        }
140        (&Method::GET, ["distribution", dist, "invalidation", id]) => {
141            Some(Route::with_two("GetInvalidation", dist, id))
142        }
143
144        // ─── Tags ───────────────────────────────────────────────────
145        (&Method::GET, ["tagging"]) => Some(Route::just("ListTagsForResource")),
146        (&Method::POST, ["tagging"]) if q.tag_op.as_deref() == Some("Tag") => {
147            Some(Route::just("TagResource"))
148        }
149        (&Method::POST, ["tagging"]) if q.tag_op.as_deref() == Some("Untag") => {
150            Some(Route::just("UntagResource"))
151        }
152
153        // ─── Monitoring Subscription ────────────────────────────────
154        (&Method::POST, ["distributions", dist, "monitoring-subscription"]) => {
155            Some(Route::with_id("CreateMonitoringSubscription", dist))
156        }
157        (&Method::GET, ["distributions", dist, "monitoring-subscription"]) => {
158            Some(Route::with_id("GetMonitoringSubscription", dist))
159        }
160        (&Method::DELETE, ["distributions", dist, "monitoring-subscription"]) => {
161            Some(Route::with_id("DeleteMonitoringSubscription", dist))
162        }
163
164        // ─── Origin Access Control ──────────────────────────────────
165        (&Method::POST, ["origin-access-control"]) => {
166            Some(Route::just("CreateOriginAccessControl"))
167        }
168        (&Method::GET, ["origin-access-control"]) => Some(Route::just("ListOriginAccessControls")),
169        (&Method::GET, ["origin-access-control", id]) => {
170            Some(Route::with_id("GetOriginAccessControl", id))
171        }
172        (&Method::GET, ["origin-access-control", id, "config"]) => {
173            Some(Route::with_id("GetOriginAccessControlConfig", id))
174        }
175        (&Method::PUT, ["origin-access-control", id, "config"]) => {
176            Some(Route::with_id("UpdateOriginAccessControl", id))
177        }
178        (&Method::DELETE, ["origin-access-control", id]) => {
179            Some(Route::with_id("DeleteOriginAccessControl", id))
180        }
181
182        // ─── Cache Policy ───────────────────────────────────────────
183        (&Method::POST, ["cache-policy"]) => Some(Route::just("CreateCachePolicy")),
184        (&Method::GET, ["cache-policy"]) => Some(Route::just("ListCachePolicies")),
185        (&Method::GET, ["cache-policy", id]) => Some(Route::with_id("GetCachePolicy", id)),
186        (&Method::GET, ["cache-policy", id, "config"]) => {
187            Some(Route::with_id("GetCachePolicyConfig", id))
188        }
189        (&Method::PUT, ["cache-policy", id]) => Some(Route::with_id("UpdateCachePolicy", id)),
190        (&Method::DELETE, ["cache-policy", id]) => Some(Route::with_id("DeleteCachePolicy", id)),
191
192        // ─── Origin Request Policy ──────────────────────────────────
193        (&Method::POST, ["origin-request-policy"]) => {
194            Some(Route::just("CreateOriginRequestPolicy"))
195        }
196        (&Method::GET, ["origin-request-policy"]) => Some(Route::just("ListOriginRequestPolicies")),
197        (&Method::GET, ["origin-request-policy", id]) => {
198            Some(Route::with_id("GetOriginRequestPolicy", id))
199        }
200        (&Method::GET, ["origin-request-policy", id, "config"]) => {
201            Some(Route::with_id("GetOriginRequestPolicyConfig", id))
202        }
203        (&Method::PUT, ["origin-request-policy", id]) => {
204            Some(Route::with_id("UpdateOriginRequestPolicy", id))
205        }
206        (&Method::DELETE, ["origin-request-policy", id]) => {
207            Some(Route::with_id("DeleteOriginRequestPolicy", id))
208        }
209
210        // ─── Response Headers Policy ────────────────────────────────
211        (&Method::POST, ["response-headers-policy"]) => {
212            Some(Route::just("CreateResponseHeadersPolicy"))
213        }
214        (&Method::GET, ["response-headers-policy"]) => {
215            Some(Route::just("ListResponseHeadersPolicies"))
216        }
217        (&Method::GET, ["response-headers-policy", id]) => {
218            Some(Route::with_id("GetResponseHeadersPolicy", id))
219        }
220        (&Method::GET, ["response-headers-policy", id, "config"]) => {
221            Some(Route::with_id("GetResponseHeadersPolicyConfig", id))
222        }
223        (&Method::PUT, ["response-headers-policy", id]) => {
224            Some(Route::with_id("UpdateResponseHeadersPolicy", id))
225        }
226        (&Method::DELETE, ["response-headers-policy", id]) => {
227            Some(Route::with_id("DeleteResponseHeadersPolicy", id))
228        }
229
230        // ─── Continuous Deployment Policy ───────────────────────────
231        (&Method::POST, ["continuous-deployment-policy"]) => {
232            Some(Route::just("CreateContinuousDeploymentPolicy"))
233        }
234        (&Method::GET, ["continuous-deployment-policy"]) => {
235            Some(Route::just("ListContinuousDeploymentPolicies"))
236        }
237        (&Method::GET, ["continuous-deployment-policy", id]) => {
238            Some(Route::with_id("GetContinuousDeploymentPolicy", id))
239        }
240        (&Method::GET, ["continuous-deployment-policy", id, "config"]) => {
241            Some(Route::with_id("GetContinuousDeploymentPolicyConfig", id))
242        }
243        (&Method::PUT, ["continuous-deployment-policy", id]) => {
244            Some(Route::with_id("UpdateContinuousDeploymentPolicy", id))
245        }
246        (&Method::DELETE, ["continuous-deployment-policy", id]) => {
247            Some(Route::with_id("DeleteContinuousDeploymentPolicy", id))
248        }
249
250        // ─── CloudFront Functions ───────────────────────────────────
251        (&Method::POST, ["function"]) => Some(Route::just("CreateFunction")),
252        (&Method::GET, ["function"]) => Some(Route::just("ListFunctions")),
253        (&Method::GET, ["function", name]) => Some(Route::with_id("GetFunction", name)),
254        (&Method::GET, ["function", name, "describe"]) => {
255            Some(Route::with_id("DescribeFunction", name))
256        }
257        (&Method::PUT, ["function", name]) => Some(Route::with_id("UpdateFunction", name)),
258        (&Method::DELETE, ["function", name]) => Some(Route::with_id("DeleteFunction", name)),
259        (&Method::POST, ["function", name, "publish"]) => {
260            Some(Route::with_id("PublishFunction", name))
261        }
262        (&Method::POST, ["function", name, "test"]) => Some(Route::with_id("TestFunction", name)),
263
264        // ─── Public Keys ────────────────────────────────────────────
265        (&Method::POST, ["public-key"]) => Some(Route::just("CreatePublicKey")),
266        (&Method::GET, ["public-key"]) => Some(Route::just("ListPublicKeys")),
267        (&Method::GET, ["public-key", id]) => Some(Route::with_id("GetPublicKey", id)),
268        (&Method::GET, ["public-key", id, "config"]) => {
269            Some(Route::with_id("GetPublicKeyConfig", id))
270        }
271        (&Method::PUT, ["public-key", id, "config"]) => Some(Route::with_id("UpdatePublicKey", id)),
272        (&Method::DELETE, ["public-key", id]) => Some(Route::with_id("DeletePublicKey", id)),
273
274        // ─── Key Groups ─────────────────────────────────────────────
275        (&Method::POST, ["key-group"]) => Some(Route::just("CreateKeyGroup")),
276        (&Method::GET, ["key-group"]) => Some(Route::just("ListKeyGroups")),
277        (&Method::GET, ["key-group", id]) => Some(Route::with_id("GetKeyGroup", id)),
278        (&Method::GET, ["key-group", id, "config"]) => {
279            Some(Route::with_id("GetKeyGroupConfig", id))
280        }
281        (&Method::PUT, ["key-group", id]) => Some(Route::with_id("UpdateKeyGroup", id)),
282        (&Method::DELETE, ["key-group", id]) => Some(Route::with_id("DeleteKeyGroup", id)),
283
284        // ─── Key Value Stores ───────────────────────────────────────
285        (&Method::POST, ["key-value-store"]) => Some(Route::just("CreateKeyValueStore")),
286        (&Method::GET, ["key-value-store"]) => Some(Route::just("ListKeyValueStores")),
287        (&Method::GET, ["key-value-store", name]) => {
288            Some(Route::with_id("DescribeKeyValueStore", name))
289        }
290        (&Method::PUT, ["key-value-store", name]) => {
291            Some(Route::with_id("UpdateKeyValueStore", name))
292        }
293        (&Method::DELETE, ["key-value-store", name]) => {
294            Some(Route::with_id("DeleteKeyValueStore", name))
295        }
296
297        // ─── Origin Access Identity (legacy) ────────────────────────
298        (&Method::POST, ["origin-access-identity", "cloudfront"]) => {
299            Some(Route::just("CreateCloudFrontOriginAccessIdentity"))
300        }
301        (&Method::GET, ["origin-access-identity", "cloudfront"]) => {
302            Some(Route::just("ListCloudFrontOriginAccessIdentities"))
303        }
304        (&Method::GET, ["origin-access-identity", "cloudfront", id]) => {
305            Some(Route::with_id("GetCloudFrontOriginAccessIdentity", id))
306        }
307        (&Method::GET, ["origin-access-identity", "cloudfront", id, "config"]) => Some(
308            Route::with_id("GetCloudFrontOriginAccessIdentityConfig", id),
309        ),
310        (&Method::PUT, ["origin-access-identity", "cloudfront", id, "config"]) => {
311            Some(Route::with_id("UpdateCloudFrontOriginAccessIdentity", id))
312        }
313        (&Method::DELETE, ["origin-access-identity", "cloudfront", id]) => {
314            Some(Route::with_id("DeleteCloudFrontOriginAccessIdentity", id))
315        }
316
317        // ─── Streaming Distribution (legacy) ────────────────────────
318        (&Method::POST, ["streaming-distribution"]) if q.with_tags => {
319            Some(Route::just("CreateStreamingDistributionWithTags").flag_with_tags())
320        }
321        (&Method::POST, ["streaming-distribution"]) => {
322            Some(Route::just("CreateStreamingDistribution"))
323        }
324        (&Method::GET, ["streaming-distribution"]) => {
325            Some(Route::just("ListStreamingDistributions"))
326        }
327        (&Method::GET, ["streaming-distribution", id]) => {
328            Some(Route::with_id("GetStreamingDistribution", id))
329        }
330        (&Method::GET, ["streaming-distribution", id, "config"]) => {
331            Some(Route::with_id("GetStreamingDistributionConfig", id))
332        }
333        (&Method::PUT, ["streaming-distribution", id, "config"]) => {
334            Some(Route::with_id("UpdateStreamingDistribution", id))
335        }
336        (&Method::DELETE, ["streaming-distribution", id]) => {
337            Some(Route::with_id("DeleteStreamingDistribution", id))
338        }
339
340        // ─── Field-Level Encryption ─────────────────────────────────
341        (&Method::POST, ["field-level-encryption"]) => {
342            Some(Route::just("CreateFieldLevelEncryptionConfig"))
343        }
344        (&Method::GET, ["field-level-encryption"]) => {
345            Some(Route::just("ListFieldLevelEncryptionConfigs"))
346        }
347        (&Method::GET, ["field-level-encryption", id]) => {
348            Some(Route::with_id("GetFieldLevelEncryption", id))
349        }
350        (&Method::GET, ["field-level-encryption", id, "config"]) => {
351            Some(Route::with_id("GetFieldLevelEncryptionConfig", id))
352        }
353        (&Method::PUT, ["field-level-encryption", id, "config"]) => {
354            Some(Route::with_id("UpdateFieldLevelEncryptionConfig", id))
355        }
356        (&Method::DELETE, ["field-level-encryption", id]) => {
357            Some(Route::with_id("DeleteFieldLevelEncryptionConfig", id))
358        }
359        (&Method::POST, ["field-level-encryption-profile"]) => {
360            Some(Route::just("CreateFieldLevelEncryptionProfile"))
361        }
362        (&Method::GET, ["field-level-encryption-profile"]) => {
363            Some(Route::just("ListFieldLevelEncryptionProfiles"))
364        }
365        (&Method::GET, ["field-level-encryption-profile", id]) => {
366            Some(Route::with_id("GetFieldLevelEncryptionProfile", id))
367        }
368        (&Method::GET, ["field-level-encryption-profile", id, "config"]) => {
369            Some(Route::with_id("GetFieldLevelEncryptionProfileConfig", id))
370        }
371        (&Method::PUT, ["field-level-encryption-profile", id, "config"]) => {
372            Some(Route::with_id("UpdateFieldLevelEncryptionProfile", id))
373        }
374        (&Method::DELETE, ["field-level-encryption-profile", id]) => {
375            Some(Route::with_id("DeleteFieldLevelEncryptionProfile", id))
376        }
377
378        // ─── Real-time Log Configs ──────────────────────────────────
379        (&Method::POST, ["realtime-log-config"]) => Some(Route::just("CreateRealtimeLogConfig")),
380        (&Method::GET, ["realtime-log-config"]) => Some(Route::just("ListRealtimeLogConfigs")),
381        (&Method::PUT, ["realtime-log-config"]) => Some(Route::just("UpdateRealtimeLogConfig")),
382        (&Method::POST, ["get-realtime-log-config"]) => Some(Route::just("GetRealtimeLogConfig")),
383        (&Method::POST, ["delete-realtime-log-config"]) => {
384            Some(Route::just("DeleteRealtimeLogConfig"))
385        }
386
387        // ─── Resource Policy ────────────────────────────────────────
388        (&Method::POST, ["get-resource-policy"]) => Some(Route::just("GetResourcePolicy")),
389        (&Method::POST, ["put-resource-policy"]) => Some(Route::just("PutResourcePolicy")),
390        (&Method::POST, ["delete-resource-policy"]) => Some(Route::just("DeleteResourcePolicy")),
391
392        // ─── VPC Origins ────────────────────────────────────────────
393        (&Method::POST, ["vpc-origin"]) => Some(Route::just("CreateVpcOrigin")),
394        (&Method::GET, ["vpc-origin"]) => Some(Route::just("ListVpcOrigins")),
395        (&Method::GET, ["vpc-origin", id]) => Some(Route::with_id("GetVpcOrigin", id)),
396        (&Method::PUT, ["vpc-origin", id]) => Some(Route::with_id("UpdateVpcOrigin", id)),
397        (&Method::DELETE, ["vpc-origin", id]) => Some(Route::with_id("DeleteVpcOrigin", id)),
398
399        // ─── Anycast IP Lists ───────────────────────────────────────
400        (&Method::POST, ["anycast-ip-list"]) => Some(Route::just("CreateAnycastIpList")),
401        (&Method::GET, ["anycast-ip-list"]) => Some(Route::just("ListAnycastIpLists")),
402        (&Method::GET, ["anycast-ip-list", id]) => Some(Route::with_id("GetAnycastIpList", id)),
403        (&Method::PUT, ["anycast-ip-list", id]) => Some(Route::with_id("UpdateAnycastIpList", id)),
404        (&Method::DELETE, ["anycast-ip-list", id]) => {
405            Some(Route::with_id("DeleteAnycastIpList", id))
406        }
407
408        // ─── Trust Stores ───────────────────────────────────────────
409        (&Method::POST, ["trust-store"]) => Some(Route::just("CreateTrustStore")),
410        (&Method::POST, ["trust-stores"]) => Some(Route::just("ListTrustStores")),
411        (&Method::GET, ["trust-store", id]) => Some(Route::with_id("GetTrustStore", id)),
412        (&Method::PUT, ["trust-store", id]) => Some(Route::with_id("UpdateTrustStore", id)),
413        (&Method::DELETE, ["trust-store", id]) => Some(Route::with_id("DeleteTrustStore", id)),
414
415        // ─── Distribution Tenants ───────────────────────────────────
416        (&Method::POST, ["distribution-tenant"]) => Some(Route::just("CreateDistributionTenant")),
417        (&Method::GET, ["distribution-tenant"]) => {
418            Some(Route::just("GetDistributionTenantByDomain"))
419        }
420        (&Method::GET, ["distribution-tenant", id]) => {
421            Some(Route::with_id("GetDistributionTenant", id))
422        }
423        (&Method::PUT, ["distribution-tenant", id]) => {
424            Some(Route::with_id("UpdateDistributionTenant", id))
425        }
426        (&Method::DELETE, ["distribution-tenant", id]) => {
427            Some(Route::with_id("DeleteDistributionTenant", id))
428        }
429        (&Method::POST, ["distribution-tenants"]) => Some(Route::just("ListDistributionTenants")),
430        (&Method::POST, ["distribution-tenants-by-customization"]) => {
431            Some(Route::just("ListDistributionTenantsByCustomization"))
432        }
433        (&Method::PUT, ["distribution-tenant", id, "associate-web-acl"]) => {
434            Some(Route::with_id("AssociateDistributionTenantWebACL", id))
435        }
436        (&Method::PUT, ["distribution-tenant", id, "disassociate-web-acl"]) => {
437            Some(Route::with_id("DisassociateDistributionTenantWebACL", id))
438        }
439        (&Method::POST, ["distribution-tenant", dist, "invalidation"]) => Some(Route::with_id(
440            "CreateInvalidationForDistributionTenant",
441            dist,
442        )),
443        (&Method::GET, ["distribution-tenant", dist, "invalidation"]) => Some(Route::with_id(
444            "ListInvalidationsForDistributionTenant",
445            dist,
446        )),
447        (&Method::GET, ["distribution-tenant", dist, "invalidation", id]) => Some(Route::with_two(
448            "GetInvalidationForDistributionTenant",
449            dist,
450            id,
451        )),
452        (&Method::POST, ["domain-association"]) => Some(Route::just("UpdateDomainAssociation")),
453        (&Method::POST, ["domain-conflicts"]) => Some(Route::just("ListDomainConflicts")),
454        (&Method::POST, ["verify-dns-configuration"]) => {
455            Some(Route::just("VerifyDnsConfiguration"))
456        }
457        (&Method::GET, ["managed-certificate", id]) => {
458            Some(Route::with_id("GetManagedCertificateDetails", id))
459        }
460
461        // ─── Connection Functions / Groups ──────────────────────────
462        (&Method::POST, ["connection-function"]) => Some(Route::just("CreateConnectionFunction")),
463        (&Method::POST, ["connection-functions"]) => Some(Route::just("ListConnectionFunctions")),
464        (&Method::GET, ["connection-function", id]) => {
465            Some(Route::with_id("GetConnectionFunction", id))
466        }
467        (&Method::GET, ["connection-function", id, "describe"]) => {
468            Some(Route::with_id("DescribeConnectionFunction", id))
469        }
470        (&Method::PUT, ["connection-function", id]) => {
471            Some(Route::with_id("UpdateConnectionFunction", id))
472        }
473        (&Method::DELETE, ["connection-function", id]) => {
474            Some(Route::with_id("DeleteConnectionFunction", id))
475        }
476        (&Method::POST, ["connection-function", id, "publish"]) => {
477            Some(Route::with_id("PublishConnectionFunction", id))
478        }
479        (&Method::POST, ["connection-function", id, "test"]) => {
480            Some(Route::with_id("TestConnectionFunction", id))
481        }
482        (&Method::POST, ["connection-group"]) => Some(Route::just("CreateConnectionGroup")),
483        (&Method::POST, ["connection-groups"]) => Some(Route::just("ListConnectionGroups")),
484        (&Method::GET, ["connection-group"]) => {
485            Some(Route::just("GetConnectionGroupByRoutingEndpoint"))
486        }
487        (&Method::GET, ["connection-group", id]) => Some(Route::with_id("GetConnectionGroup", id)),
488        (&Method::PUT, ["connection-group", id]) => {
489            Some(Route::with_id("UpdateConnectionGroup", id))
490        }
491        (&Method::DELETE, ["connection-group", id]) => {
492            Some(Route::with_id("DeleteConnectionGroup", id))
493        }
494
495        _ => None,
496    }
497}
498
499#[derive(Default, Debug)]
500struct QueryFlags {
501    with_tags: bool,
502    tag_op: Option<String>,
503}
504
505impl QueryFlags {
506    fn parse(raw: &str) -> Self {
507        let mut out = QueryFlags::default();
508        for pair in raw.split('&').filter(|p| !p.is_empty()) {
509            if pair == "WithTags" || pair.starts_with("WithTags=") {
510                out.with_tags = true;
511                continue;
512            }
513            if let Some(rest) = pair.strip_prefix("Operation=") {
514                let value = decode(rest);
515                out.tag_op = Some(value);
516            }
517        }
518        out
519    }
520}
521
522fn decode(s: &str) -> String {
523    // Minimal percent-decode (CloudFront only sends ASCII operation values).
524    let mut out = String::with_capacity(s.len());
525    let mut bytes = s.bytes();
526    while let Some(b) = bytes.next() {
527        if b == b'%' {
528            let h1 = bytes.next();
529            let h2 = bytes.next();
530            if let (Some(a), Some(b2)) = (h1, h2) {
531                if let (Some(a), Some(b2)) = (hex(a), hex(b2)) {
532                    out.push(((a << 4) | b2) as char);
533                    continue;
534                }
535            }
536            out.push('%');
537        } else if b == b'+' {
538            out.push(' ');
539        } else {
540            out.push(b as char);
541        }
542    }
543    out
544}
545
546fn hex(b: u8) -> Option<u8> {
547    match b {
548        b'0'..=b'9' => Some(b - b'0'),
549        b'a'..=b'f' => Some(b - b'a' + 10),
550        b'A'..=b'F' => Some(b - b'A' + 10),
551        _ => None,
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    #[test]
560    fn create_distribution() {
561        let r = route(&Method::POST, "/2020-05-31/distribution", "").unwrap();
562        assert_eq!(r.action, "CreateDistribution");
563    }
564
565    #[test]
566    fn create_distribution_with_tags() {
567        let r = route(&Method::POST, "/2020-05-31/distribution", "WithTags").unwrap();
568        assert_eq!(r.action, "CreateDistributionWithTags");
569        assert!(r.with_tags);
570    }
571
572    #[test]
573    fn get_distribution() {
574        let r = route(&Method::GET, "/2020-05-31/distribution/EDFDVBD632BHDS5", "").unwrap();
575        assert_eq!(r.action, "GetDistribution");
576        assert_eq!(r.id.as_deref(), Some("EDFDVBD632BHDS5"));
577    }
578
579    #[test]
580    fn trailing_slash_collection_routes_to_list() {
581        // A trailing slash on the collection root must list, not
582        // GetDistribution(id="") -> NoSuchDistribution (1.1).
583        let r = route(&Method::GET, "/2020-05-31/distribution/", "").unwrap();
584        assert_eq!(r.action, "ListDistributions");
585    }
586
587    #[test]
588    fn create_invalidation() {
589        let r = route(
590            &Method::POST,
591            "/2020-05-31/distribution/EDFDVBD632BHDS5/invalidation",
592            "",
593        )
594        .unwrap();
595        assert_eq!(r.action, "CreateInvalidation");
596        assert_eq!(r.id.as_deref(), Some("EDFDVBD632BHDS5"));
597    }
598
599    #[test]
600    fn get_invalidation() {
601        let r = route(
602            &Method::GET,
603            "/2020-05-31/distribution/EDFDVBD632BHDS5/invalidation/IDFDVBD632BHDS5",
604            "",
605        )
606        .unwrap();
607        assert_eq!(r.action, "GetInvalidation");
608        assert_eq!(r.id.as_deref(), Some("EDFDVBD632BHDS5"));
609        assert_eq!(r.second_id.as_deref(), Some("IDFDVBD632BHDS5"));
610    }
611
612    #[test]
613    fn tag_resource() {
614        let r = route(
615            &Method::POST,
616            "/2020-05-31/tagging",
617            "Operation=Tag&Resource=arn:aws:cloudfront",
618        )
619        .unwrap();
620        assert_eq!(r.action, "TagResource");
621    }
622
623    #[test]
624    fn untag_resource() {
625        let r = route(
626            &Method::POST,
627            "/2020-05-31/tagging",
628            "Operation=Untag&Resource=arn:aws:cloudfront",
629        )
630        .unwrap();
631        assert_eq!(r.action, "UntagResource");
632    }
633
634    #[test]
635    fn list_tags() {
636        let r = route(
637            &Method::GET,
638            "/2020-05-31/tagging",
639            "Resource=arn:aws:cloudfront",
640        )
641        .unwrap();
642        assert_eq!(r.action, "ListTagsForResource");
643    }
644
645    #[test]
646    fn unknown_returns_none() {
647        assert!(route(&Method::GET, "/2020-05-31/totally-bogus", "").is_none());
648    }
649}