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