Skip to main content

fakecloud_cloudfront/
extras_service.rs

1//! Handlers for CloudFront Batch 6a: VPC Origins, Anycast IP Lists,
2//! Trust Stores, Resource Policies.
3
4use chrono::Utc;
5use http::{HeaderMap, StatusCode};
6
7use fakecloud_aws::arn::Arn;
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
9
10use crate::extras::{
11    CaCertificatesBundleSource, CreateAnycastIpListRequest, CreateTrustStoreRequest,
12    CreateVpcOriginRequest, ResourcePolicyRequest, StoredAnycastIpList, StoredResourcePolicy,
13    StoredTrustStore, StoredVpcOrigin, UpdateAnycastIpListRequest, VpcOriginEndpointConfig,
14};
15use crate::policies::{
16    not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
17};
18use crate::router::Route;
19use crate::service::{
20    aws_error, esc, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
21    DEFAULT_ACCOUNT,
22};
23use crate::xml_io;
24
25const NS: &str = crate::NAMESPACE;
26const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
27
28// ─── VPC Origin ───────────────────────────────────────────────────────
29
30impl CloudFrontService {
31    pub(crate) fn create_vpc_origin(
32        &self,
33        req: &AwsRequest,
34    ) -> Result<AwsResponse, AwsServiceError> {
35        let parsed: CreateVpcOriginRequest = xml_io::from_xml_root(&req.body)
36            .map_err(|e| invalid_argument(format!("invalid CreateVpcOriginRequest XML: {e}")))?;
37        let cfg = parsed.vpc_origin_endpoint_config;
38        if cfg.name.is_empty() {
39            return Err(invalid_argument("Name is required"));
40        }
41        if cfg.arn.is_empty() {
42            return Err(invalid_argument("Arn is required"));
43        }
44        let mut state = self.state.write();
45        let account = state
46            .accounts
47            .entry(DEFAULT_ACCOUNT.to_string())
48            .or_default();
49        if account
50            .vpc_origins
51            .values()
52            .any(|v| v.config.name == cfg.name)
53        {
54            return Err(aws_error(
55                StatusCode::CONFLICT,
56                "EntityAlreadyExists",
57                format!("VpcOrigin {} already exists", cfg.name),
58            ));
59        }
60        let id = generate_id_with_prefix("VO");
61        let etag = generate_id_with_prefix("E");
62        let now = Utc::now();
63        let arn =
64            Arn::global("cloudfront", DEFAULT_ACCOUNT, &format!("vpc-origin/{id}")).to_string();
65        let stored = StoredVpcOrigin {
66            id: id.clone(),
67            arn,
68            status: "Deployed".to_string(),
69            etag: etag.clone(),
70            created_time: now,
71            last_modified_time: now,
72            config: cfg,
73        };
74        account.vpc_origins.insert(id.clone(), stored.clone());
75        drop(state);
76        let body = render_vpc_origin(&stored);
77        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
78    }
79
80    pub(crate) fn get_vpc_origin(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
81        let id = route_id(route, "VpcOrigin")?;
82        let state = self.state.read();
83        let v = state
84            .accounts
85            .get(DEFAULT_ACCOUNT)
86            .and_then(|a| a.vpc_origins.get(&id).cloned())
87            .ok_or_else(|| not_found("VpcOrigin", &id))?;
88        drop(state);
89        let body = render_vpc_origin(&v);
90        Ok(xml_with_etag(StatusCode::OK, body, &v.etag, None))
91    }
92
93    pub(crate) fn update_vpc_origin(
94        &self,
95        req: &AwsRequest,
96        route: &Route,
97    ) -> Result<AwsResponse, AwsServiceError> {
98        let id = route_id(route, "VpcOrigin")?;
99        let if_match = require_if_match(req)?;
100        let cfg: VpcOriginEndpointConfig = xml_io::from_xml_root(&req.body)
101            .map_err(|e| invalid_argument(format!("invalid VpcOriginEndpointConfig XML: {e}")))?;
102        let mut state = self.state.write();
103        let account = state
104            .accounts
105            .get_mut(DEFAULT_ACCOUNT)
106            .ok_or_else(|| not_found("VpcOrigin", &id))?;
107        let v = account
108            .vpc_origins
109            .get_mut(&id)
110            .ok_or_else(|| not_found("VpcOrigin", &id))?;
111        if v.etag != if_match {
112            return Err(precondition_failed());
113        }
114        if cfg.name.is_empty() {
115            return Err(invalid_argument("Name is required"));
116        }
117        if cfg.arn.is_empty() {
118            return Err(invalid_argument("Arn is required"));
119        }
120        v.config = cfg;
121        v.etag = generate_id_with_prefix("E");
122        v.last_modified_time = Utc::now();
123        let snap = v.clone();
124        drop(state);
125        let body = render_vpc_origin(&snap);
126        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
127    }
128
129    pub(crate) fn delete_vpc_origin(
130        &self,
131        req: &AwsRequest,
132        route: &Route,
133    ) -> Result<AwsResponse, AwsServiceError> {
134        let id = route_id(route, "VpcOrigin")?;
135        let if_match = require_if_match(req)?;
136        let mut state = self.state.write();
137        let account = state
138            .accounts
139            .get_mut(DEFAULT_ACCOUNT)
140            .ok_or_else(|| not_found("VpcOrigin", &id))?;
141        let v = account
142            .vpc_origins
143            .get(&id)
144            .ok_or_else(|| not_found("VpcOrigin", &id))?;
145        if v.etag != if_match {
146            return Err(precondition_failed());
147        }
148        let arn = v.arn.clone();
149        let snap = v.clone();
150        account.vpc_origins.remove(&id);
151        account.tags.remove(&arn);
152        drop(state);
153        let body = render_vpc_origin(&snap);
154        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
155    }
156
157    pub(crate) fn list_vpc_origins(
158        &self,
159        _req: &AwsRequest,
160    ) -> Result<AwsResponse, AwsServiceError> {
161        let state = self.state.read();
162        let mut items: Vec<StoredVpcOrigin> = state
163            .accounts
164            .get(DEFAULT_ACCOUNT)
165            .map(|a| a.vpc_origins.values().cloned().collect())
166            .unwrap_or_default();
167        drop(state);
168        items.sort_by(|a, b| a.id.cmp(&b.id));
169
170        let mut body = String::with_capacity(512);
171        body.push_str(XML_DECL);
172        body.push_str(&format!("<VpcOriginList xmlns=\"{NS}\">"));
173        body.push_str("<Marker></Marker>");
174        body.push_str("<MaxItems>100</MaxItems>");
175        body.push_str(&format!("<IsTruncated>{}</IsTruncated>", false));
176        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
177        body.push_str("<Items>");
178        for v in &items {
179            body.push_str("<VpcOriginSummary>");
180            body.push_str(&format!("<Id>{}</Id>", esc(&v.id)));
181            body.push_str(&format!("<Name>{}</Name>", esc(&v.config.name)));
182            body.push_str(&format!("<Status>{}</Status>", esc(&v.status)));
183            body.push_str(&format!(
184                "<CreatedTime>{}</CreatedTime>",
185                rfc3339(&v.created_time)
186            ));
187            body.push_str(&format!(
188                "<LastModifiedTime>{}</LastModifiedTime>",
189                rfc3339(&v.last_modified_time)
190            ));
191            body.push_str(&format!("<Arn>{}</Arn>", esc(&v.arn)));
192            body.push_str(&format!(
193                "<OriginEndpointArn>{}</OriginEndpointArn>",
194                esc(&v.config.arn)
195            ));
196            body.push_str("</VpcOriginSummary>");
197        }
198        body.push_str("</Items>");
199        body.push_str("</VpcOriginList>");
200        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
201    }
202}
203
204// ─── Anycast IP List ──────────────────────────────────────────────────
205
206impl CloudFrontService {
207    pub(crate) fn create_anycast_ip_list(
208        &self,
209        req: &AwsRequest,
210    ) -> Result<AwsResponse, AwsServiceError> {
211        let cfg: CreateAnycastIpListRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
212            invalid_argument(format!("invalid CreateAnycastIpListRequest XML: {e}"))
213        })?;
214        if cfg.name.is_empty() {
215            return Err(invalid_argument("Name is required"));
216        }
217        if cfg.ip_count != 3 && cfg.ip_count != 21 {
218            return Err(invalid_argument("IpCount must be 3 or 21"));
219        }
220        let mut state = self.state.write();
221        let account = state
222            .accounts
223            .entry(DEFAULT_ACCOUNT.to_string())
224            .or_default();
225        if account
226            .anycast_ip_lists
227            .values()
228            .any(|a| a.name == cfg.name)
229        {
230            return Err(aws_error(
231                StatusCode::CONFLICT,
232                "EntityAlreadyExists",
233                format!("AnycastIpList {} already exists", cfg.name),
234            ));
235        }
236        let id = generate_id_with_prefix("AIL");
237        let arn = format!(
238            "arn:aws:cloudfront::{}:anycast-ip-list/{}",
239            DEFAULT_ACCOUNT, id
240        );
241        // Synthesize deterministic ipv4 addresses for the list.
242        let anycast_ips: Vec<String> = (0..cfg.ip_count)
243            .map(|i| format!("198.51.100.{}", (i + 1) as u8))
244            .collect();
245        let etag = generate_id_with_prefix("E");
246        let stored = StoredAnycastIpList {
247            id: id.clone(),
248            name: cfg.name,
249            status: "Deployed".to_string(),
250            arn,
251            ip_count: cfg.ip_count,
252            ip_address_type: cfg.ip_address_type,
253            anycast_ips,
254            last_modified_time: Utc::now(),
255            etag: etag.clone(),
256        };
257        account.anycast_ip_lists.insert(id.clone(), stored.clone());
258        drop(state);
259        let body = render_anycast_ip_list(&stored);
260        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
261    }
262
263    pub(crate) fn get_anycast_ip_list(
264        &self,
265        route: &Route,
266    ) -> Result<AwsResponse, AwsServiceError> {
267        let id = route_id(route, "AnycastIpList")?;
268        let state = self.state.read();
269        let a = state
270            .accounts
271            .get(DEFAULT_ACCOUNT)
272            .and_then(|a| a.anycast_ip_lists.get(&id).cloned())
273            .ok_or_else(|| not_found("AnycastIpList", &id))?;
274        drop(state);
275        let body = render_anycast_ip_list(&a);
276        Ok(xml_with_etag(StatusCode::OK, body, &a.etag, None))
277    }
278
279    pub(crate) fn update_anycast_ip_list(
280        &self,
281        req: &AwsRequest,
282        route: &Route,
283    ) -> Result<AwsResponse, AwsServiceError> {
284        let id = route_id(route, "AnycastIpList")?;
285        let if_match = require_if_match(req)?;
286        let cfg: UpdateAnycastIpListRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
287            invalid_argument(format!("invalid UpdateAnycastIpListRequest XML: {e}"))
288        })?;
289        let mut state = self.state.write();
290        let account = state
291            .accounts
292            .get_mut(DEFAULT_ACCOUNT)
293            .ok_or_else(|| not_found("AnycastIpList", &id))?;
294        let a = account
295            .anycast_ip_lists
296            .get_mut(&id)
297            .ok_or_else(|| not_found("AnycastIpList", &id))?;
298        if a.etag != if_match {
299            return Err(precondition_failed());
300        }
301        if let Some(t) = cfg.ip_address_type {
302            a.ip_address_type = Some(t);
303        }
304        a.last_modified_time = Utc::now();
305        a.etag = generate_id_with_prefix("E");
306        let snap = a.clone();
307        drop(state);
308        let body = render_anycast_ip_list(&snap);
309        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
310    }
311
312    pub(crate) fn delete_anycast_ip_list(
313        &self,
314        req: &AwsRequest,
315        route: &Route,
316    ) -> Result<AwsResponse, AwsServiceError> {
317        let id = route_id(route, "AnycastIpList")?;
318        let if_match = require_if_match(req)?;
319        let mut state = self.state.write();
320        let account = state
321            .accounts
322            .get_mut(DEFAULT_ACCOUNT)
323            .ok_or_else(|| not_found("AnycastIpList", &id))?;
324        let a = account
325            .anycast_ip_lists
326            .get(&id)
327            .ok_or_else(|| not_found("AnycastIpList", &id))?;
328        if a.etag != if_match {
329            return Err(precondition_failed());
330        }
331        let arn = a.arn.clone();
332        account.anycast_ip_lists.remove(&id);
333        account.tags.remove(&arn);
334        drop(state);
335        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
336    }
337
338    pub(crate) fn list_anycast_ip_lists(
339        &self,
340        _req: &AwsRequest,
341    ) -> Result<AwsResponse, AwsServiceError> {
342        let state = self.state.read();
343        let mut items: Vec<StoredAnycastIpList> = state
344            .accounts
345            .get(DEFAULT_ACCOUNT)
346            .map(|a| a.anycast_ip_lists.values().cloned().collect())
347            .unwrap_or_default();
348        drop(state);
349        items.sort_by(|a, b| a.id.cmp(&b.id));
350
351        let mut body = String::with_capacity(512);
352        body.push_str(XML_DECL);
353        body.push_str(&format!("<AnycastIpListCollection xmlns=\"{NS}\">"));
354        body.push_str("<Marker></Marker>");
355        body.push_str("<MaxItems>100</MaxItems>");
356        body.push_str(&format!("<IsTruncated>{}</IsTruncated>", false));
357        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
358        body.push_str("<Items>");
359        for a in &items {
360            body.push_str("<AnycastIpListSummary>");
361            push_anycast_summary(&mut body, a);
362            body.push_str("</AnycastIpListSummary>");
363        }
364        body.push_str("</Items>");
365        body.push_str("</AnycastIpListCollection>");
366        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
367    }
368}
369
370// ─── Trust Store ──────────────────────────────────────────────────────
371
372impl CloudFrontService {
373    pub(crate) fn create_trust_store(
374        &self,
375        req: &AwsRequest,
376    ) -> Result<AwsResponse, AwsServiceError> {
377        let cfg: CreateTrustStoreRequest = xml_io::from_xml_root(&req.body)
378            .map_err(|e| invalid_argument(format!("invalid CreateTrustStoreRequest XML: {e}")))?;
379        if cfg.name.is_empty() {
380            return Err(invalid_argument("Name is required"));
381        }
382        if cfg
383            .ca_certificates_bundle_source
384            .ca_certificates_bundle_s3_location
385            .is_none()
386        {
387            return Err(invalid_argument(
388                "CaCertificatesBundleSource must specify a non-empty member",
389            ));
390        }
391        let mut state = self.state.write();
392        let account = state
393            .accounts
394            .entry(DEFAULT_ACCOUNT.to_string())
395            .or_default();
396        if account.trust_stores.values().any(|t| t.name == cfg.name) {
397            return Err(aws_error(
398                StatusCode::CONFLICT,
399                "EntityAlreadyExists",
400                format!("TrustStore {} already exists", cfg.name),
401            ));
402        }
403        let id = generate_id_with_prefix("TS");
404        let arn =
405            Arn::global("cloudfront", DEFAULT_ACCOUNT, &format!("trust-store/{id}")).to_string();
406        let etag = generate_id_with_prefix("E");
407        let stored = StoredTrustStore {
408            id: id.clone(),
409            arn,
410            name: cfg.name,
411            etag: etag.clone(),
412            last_modified_time: Utc::now(),
413            ca_certificates_bundle_source: cfg.ca_certificates_bundle_source,
414        };
415        account.trust_stores.insert(id.clone(), stored.clone());
416        drop(state);
417        let body = render_trust_store(&stored);
418        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
419    }
420
421    pub(crate) fn get_trust_store(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
422        let id = route_id(route, "TrustStore")?;
423        let state = self.state.read();
424        let t = state
425            .accounts
426            .get(DEFAULT_ACCOUNT)
427            .and_then(|a| a.trust_stores.get(&id).cloned())
428            .ok_or_else(|| not_found("TrustStore", &id))?;
429        drop(state);
430        let body = render_trust_store(&t);
431        Ok(xml_with_etag(StatusCode::OK, body, &t.etag, None))
432    }
433
434    pub(crate) fn update_trust_store(
435        &self,
436        req: &AwsRequest,
437        route: &Route,
438    ) -> Result<AwsResponse, AwsServiceError> {
439        let id = route_id(route, "TrustStore")?;
440        let if_match = require_if_match(req)?;
441        let bundle: CaCertificatesBundleSource = xml_io::from_xml_root(&req.body).map_err(|e| {
442            invalid_argument(format!("invalid CaCertificatesBundleSource XML: {e}"))
443        })?;
444        if bundle.ca_certificates_bundle_s3_location.is_none() {
445            return Err(invalid_argument(
446                "CaCertificatesBundleSource must specify a non-empty member",
447            ));
448        }
449        let mut state = self.state.write();
450        let account = state
451            .accounts
452            .get_mut(DEFAULT_ACCOUNT)
453            .ok_or_else(|| not_found("TrustStore", &id))?;
454        let t = account
455            .trust_stores
456            .get_mut(&id)
457            .ok_or_else(|| not_found("TrustStore", &id))?;
458        if t.etag != if_match {
459            return Err(precondition_failed());
460        }
461        t.ca_certificates_bundle_source = bundle;
462        t.last_modified_time = Utc::now();
463        t.etag = generate_id_with_prefix("E");
464        let snap = t.clone();
465        drop(state);
466        let body = render_trust_store(&snap);
467        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
468    }
469
470    pub(crate) fn delete_trust_store(
471        &self,
472        req: &AwsRequest,
473        route: &Route,
474    ) -> Result<AwsResponse, AwsServiceError> {
475        let id = route_id(route, "TrustStore")?;
476        let if_match = require_if_match(req)?;
477        let mut state = self.state.write();
478        let account = state
479            .accounts
480            .get_mut(DEFAULT_ACCOUNT)
481            .ok_or_else(|| not_found("TrustStore", &id))?;
482        let t = account
483            .trust_stores
484            .get(&id)
485            .ok_or_else(|| not_found("TrustStore", &id))?;
486        if t.etag != if_match {
487            return Err(precondition_failed());
488        }
489        let arn = t.arn.clone();
490        account.trust_stores.remove(&id);
491        account.tags.remove(&arn);
492        drop(state);
493        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
494    }
495
496    pub(crate) fn list_trust_stores(
497        &self,
498        _req: &AwsRequest,
499    ) -> Result<AwsResponse, AwsServiceError> {
500        let state = self.state.read();
501        let mut items: Vec<StoredTrustStore> = state
502            .accounts
503            .get(DEFAULT_ACCOUNT)
504            .map(|a| a.trust_stores.values().cloned().collect())
505            .unwrap_or_default();
506        drop(state);
507        items.sort_by(|a, b| a.id.cmp(&b.id));
508
509        let mut body = String::with_capacity(512);
510        body.push_str(XML_DECL);
511        body.push_str(&format!("<ListTrustStoresResult xmlns=\"{NS}\">"));
512        body.push_str("<TrustStoreList>");
513        for t in &items {
514            body.push_str("<TrustStoreSummary>");
515            body.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
516            body.push_str(&format!("<Arn>{}</Arn>", esc(&t.arn)));
517            body.push_str(&format!("<Name>{}</Name>", esc(&t.name)));
518            body.push_str(&format!(
519                "<LastModifiedTime>{}</LastModifiedTime>",
520                rfc3339(&t.last_modified_time)
521            ));
522            body.push_str("</TrustStoreSummary>");
523        }
524        body.push_str("</TrustStoreList>");
525        body.push_str("</ListTrustStoresResult>");
526        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
527    }
528}
529
530// ─── Resource Policy ──────────────────────────────────────────────────
531
532impl CloudFrontService {
533    pub(crate) fn put_resource_policy(
534        &self,
535        req: &AwsRequest,
536    ) -> Result<AwsResponse, AwsServiceError> {
537        let parsed: ResourcePolicyRequest = xml_io::from_xml_root(&req.body)
538            .map_err(|e| invalid_argument(format!("invalid PutResourcePolicyRequest XML: {e}")))?;
539        if parsed.resource_arn.is_empty() {
540            return Err(invalid_argument("ResourceArn is required"));
541        }
542        let policy = parsed
543            .policy_document
544            .ok_or_else(|| invalid_argument("PolicyDocument is required"))?;
545        let mut state = self.state.write();
546        let account = state
547            .accounts
548            .entry(DEFAULT_ACCOUNT.to_string())
549            .or_default();
550        account.resource_policies.insert(
551            parsed.resource_arn.clone(),
552            StoredResourcePolicy {
553                resource_arn: parsed.resource_arn,
554                policy_document: policy,
555            },
556        );
557        drop(state);
558        let mut body = String::with_capacity(128);
559        body.push_str(XML_DECL);
560        body.push_str(&format!("<PutResourcePolicyResult xmlns=\"{NS}\"/>"));
561        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
562    }
563
564    pub(crate) fn get_resource_policy(
565        &self,
566        req: &AwsRequest,
567    ) -> Result<AwsResponse, AwsServiceError> {
568        let parsed: ResourcePolicyRequest = xml_io::from_xml_root(&req.body)
569            .map_err(|e| invalid_argument(format!("invalid GetResourcePolicyRequest XML: {e}")))?;
570        if parsed.resource_arn.is_empty() {
571            return Err(invalid_argument("ResourceArn is required"));
572        }
573        let state = self.state.read();
574        let p = state
575            .accounts
576            .get(DEFAULT_ACCOUNT)
577            .and_then(|a| a.resource_policies.get(&parsed.resource_arn).cloned())
578            .ok_or_else(|| not_found("ResourcePolicy", &parsed.resource_arn))?;
579        drop(state);
580        let mut body = String::with_capacity(512);
581        body.push_str(XML_DECL);
582        body.push_str(&format!("<GetResourcePolicyResult xmlns=\"{NS}\">"));
583        body.push_str(&format!(
584            "<ResourceArn>{}</ResourceArn>",
585            esc(&p.resource_arn)
586        ));
587        body.push_str(&format!(
588            "<PolicyDocument>{}</PolicyDocument>",
589            esc(&p.policy_document)
590        ));
591        body.push_str("</GetResourcePolicyResult>");
592        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
593    }
594
595    pub(crate) fn delete_resource_policy(
596        &self,
597        req: &AwsRequest,
598    ) -> Result<AwsResponse, AwsServiceError> {
599        let parsed: ResourcePolicyRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
600            invalid_argument(format!("invalid DeleteResourcePolicyRequest XML: {e}"))
601        })?;
602        if parsed.resource_arn.is_empty() {
603            return Err(invalid_argument("ResourceArn is required"));
604        }
605        let mut state = self.state.write();
606        let account = state
607            .accounts
608            .get_mut(DEFAULT_ACCOUNT)
609            .ok_or_else(|| not_found("ResourcePolicy", &parsed.resource_arn))?;
610        if account
611            .resource_policies
612            .remove(&parsed.resource_arn)
613            .is_none()
614        {
615            return Err(not_found("ResourcePolicy", &parsed.resource_arn));
616        }
617        drop(state);
618        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
619    }
620}
621
622// ─── XML render helpers ───────────────────────────────────────────────
623
624fn render_vpc_origin(v: &StoredVpcOrigin) -> String {
625    let mut out = String::with_capacity(512);
626    out.push_str(XML_DECL);
627    out.push_str(&format!("<VpcOrigin xmlns=\"{NS}\">"));
628    out.push_str(&format!("<Id>{}</Id>", esc(&v.id)));
629    out.push_str(&format!("<Arn>{}</Arn>", esc(&v.arn)));
630    out.push_str(&format!("<Status>{}</Status>", esc(&v.status)));
631    out.push_str(&format!(
632        "<CreatedTime>{}</CreatedTime>",
633        rfc3339(&v.created_time)
634    ));
635    out.push_str(&format!(
636        "<LastModifiedTime>{}</LastModifiedTime>",
637        rfc3339(&v.last_modified_time)
638    ));
639    out.push_str(&render_vpc_origin_endpoint_config(&v.config));
640    out.push_str("</VpcOrigin>");
641    out
642}
643
644fn render_vpc_origin_endpoint_config(c: &VpcOriginEndpointConfig) -> String {
645    let mut out = String::with_capacity(256);
646    out.push_str("<VpcOriginEndpointConfig>");
647    out.push_str(&format!("<Name>{}</Name>", esc(&c.name)));
648    out.push_str(&format!("<Arn>{}</Arn>", esc(&c.arn)));
649    out.push_str(&format!("<HTTPPort>{}</HTTPPort>", c.http_port));
650    out.push_str(&format!("<HTTPSPort>{}</HTTPSPort>", c.https_port));
651    out.push_str(&format!(
652        "<OriginProtocolPolicy>{}</OriginProtocolPolicy>",
653        esc(&c.origin_protocol_policy)
654    ));
655    if let Some(ssl) = &c.origin_ssl_protocols {
656        out.push_str("<OriginSslProtocols>");
657        out.push_str(&format!("<Quantity>{}</Quantity>", ssl.quantity));
658        out.push_str("<Items>");
659        for p in &ssl.items.ssl_protocol {
660            out.push_str(&format!("<SslProtocol>{}</SslProtocol>", esc(p)));
661        }
662        out.push_str("</Items>");
663        out.push_str("</OriginSslProtocols>");
664    }
665    out.push_str("</VpcOriginEndpointConfig>");
666    out
667}
668
669fn render_anycast_ip_list(a: &StoredAnycastIpList) -> String {
670    let mut out = String::with_capacity(512);
671    out.push_str(XML_DECL);
672    out.push_str(&format!("<AnycastIpList xmlns=\"{NS}\">"));
673    out.push_str(&format!("<Id>{}</Id>", esc(&a.id)));
674    out.push_str(&format!("<Name>{}</Name>", esc(&a.name)));
675    out.push_str(&format!("<Status>{}</Status>", esc(&a.status)));
676    out.push_str(&format!("<Arn>{}</Arn>", esc(&a.arn)));
677    if let Some(t) = &a.ip_address_type {
678        out.push_str(&format!("<IpAddressType>{}</IpAddressType>", esc(t)));
679    }
680    out.push_str("<AnycastIps>");
681    for ip in &a.anycast_ips {
682        out.push_str(&format!("<AnycastIp>{}</AnycastIp>", esc(ip)));
683    }
684    out.push_str("</AnycastIps>");
685    out.push_str(&format!("<IpCount>{}</IpCount>", a.ip_count));
686    out.push_str(&format!(
687        "<LastModifiedTime>{}</LastModifiedTime>",
688        rfc3339(&a.last_modified_time)
689    ));
690    out.push_str("</AnycastIpList>");
691    out
692}
693
694fn push_anycast_summary(out: &mut String, a: &StoredAnycastIpList) {
695    out.push_str(&format!("<Id>{}</Id>", esc(&a.id)));
696    out.push_str(&format!("<Name>{}</Name>", esc(&a.name)));
697    out.push_str(&format!("<Status>{}</Status>", esc(&a.status)));
698    out.push_str(&format!("<Arn>{}</Arn>", esc(&a.arn)));
699    if let Some(t) = &a.ip_address_type {
700        out.push_str(&format!("<IpAddressType>{}</IpAddressType>", esc(t)));
701    }
702    out.push_str(&format!("<IpCount>{}</IpCount>", a.ip_count));
703    out.push_str(&format!(
704        "<LastModifiedTime>{}</LastModifiedTime>",
705        rfc3339(&a.last_modified_time)
706    ));
707}
708
709fn render_trust_store(t: &StoredTrustStore) -> String {
710    let mut out = String::with_capacity(512);
711    out.push_str(XML_DECL);
712    out.push_str(&format!("<TrustStore xmlns=\"{NS}\">"));
713    out.push_str(&format!("<Id>{}</Id>", esc(&t.id)));
714    out.push_str(&format!("<Arn>{}</Arn>", esc(&t.arn)));
715    out.push_str(&format!("<Name>{}</Name>", esc(&t.name)));
716    out.push_str(&format!(
717        "<LastModifiedTime>{}</LastModifiedTime>",
718        rfc3339(&t.last_modified_time)
719    ));
720    out.push_str(&render_bundle_source(&t.ca_certificates_bundle_source));
721    out.push_str("</TrustStore>");
722    out
723}
724
725fn render_bundle_source(s: &CaCertificatesBundleSource) -> String {
726    let mut out = String::with_capacity(256);
727    out.push_str("<CaCertificatesBundleSource>");
728    if let Some(s3) = &s.ca_certificates_bundle_s3_location {
729        out.push_str("<CaCertificatesBundleS3Location>");
730        out.push_str(&format!("<Bucket>{}</Bucket>", esc(&s3.bucket)));
731        out.push_str(&format!("<Key>{}</Key>", esc(&s3.key)));
732        out.push_str(&format!("<Region>{}</Region>", esc(&s3.region)));
733        if let Some(v) = &s3.version {
734            out.push_str(&format!("<Version>{}</Version>", esc(v)));
735        }
736        out.push_str("</CaCertificatesBundleS3Location>");
737    }
738    out.push_str("</CaCertificatesBundleSource>");
739    out
740}