Skip to main content

fakecloud_cloudfront/
functions_service.rs

1//! Handlers for CloudFront Batch 3 resources: Functions, Public Keys,
2//! Key Groups, Key Value Stores, Origin Access Identities (legacy),
3//! Monitoring Subscriptions.
4
5use base64::Engine;
6use chrono::Utc;
7use http::header::ETAG;
8use http::{HeaderMap, StatusCode};
9use uuid::Uuid;
10
11use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
12
13use crate::functions::{
14    CloudFrontOriginAccessIdentityConfig, FunctionConfig, ImportSource, KeyGroupConfig,
15    MonitoringSubscriptionBody, PublicKeyConfig, StoredFunction, StoredKeyGroup,
16    StoredKeyValueStore, StoredMonitoringSubscription, StoredOriginAccessIdentity, StoredPublicKey,
17};
18use crate::policies::{
19    not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
20};
21use crate::router::Route;
22use crate::service::{
23    aws_error, esc, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
24    DEFAULT_ACCOUNT,
25};
26use crate::xml_io;
27
28const NS: &str = crate::NAMESPACE;
29const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
30
31// ─── CloudFront Functions ─────────────────────────────────────────────
32
33impl CloudFrontService {
34    pub(crate) fn create_function(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
35        // Body shape: <CreateFunctionRequest><Name/><FunctionConfig/><FunctionCode/></CreateFunctionRequest>
36        let parsed: CreateFunctionRequest = xml_io::from_xml_root(&req.body)
37            .map_err(|e| invalid_argument(format!("invalid CreateFunctionRequest XML: {e}")))?;
38        if parsed.name.is_empty() {
39            return Err(invalid_argument("CreateFunctionRequest.Name is required"));
40        }
41
42        let mut state = self.state.write();
43        let account = state
44            .accounts
45            .entry(DEFAULT_ACCOUNT.to_string())
46            .or_default();
47        if account.functions.contains_key(&parsed.name) {
48            return Err(aws_error(
49                StatusCode::CONFLICT,
50                "FunctionAlreadyExists",
51                format!("Function {} already exists", parsed.name),
52            ));
53        }
54        let now = Utc::now();
55        let etag = generate_id_with_prefix("E");
56        let function_arn = format!(
57            "arn:aws:cloudfront::{}:function/{}",
58            DEFAULT_ACCOUNT, parsed.name
59        );
60        let stored = StoredFunction {
61            name: parsed.name.clone(),
62            etag: etag.clone(),
63            status: "UNPUBLISHED".to_string(),
64            stage: "DEVELOPMENT".to_string(),
65            function_arn: function_arn.clone(),
66            created_time: now,
67            last_modified_time: now,
68            config: parsed.function_config,
69            function_code: parsed.function_code,
70        };
71        account
72            .functions
73            .insert(parsed.name.clone(), stored.clone());
74        drop(state);
75
76        let body = render_function_summary(&stored, "CreateFunctionResult");
77        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, None))
78    }
79
80    pub(crate) fn describe_function(
81        &self,
82        req: &AwsRequest,
83        route: &Route,
84    ) -> Result<AwsResponse, AwsServiceError> {
85        let name = route_id(route, "Function")?;
86        let stage = parse_stage_query(&req.raw_query);
87        let state = self.state.read();
88        let f = state
89            .accounts
90            .get(DEFAULT_ACCOUNT)
91            .and_then(|a| a.functions.get(&name).cloned())
92            .ok_or_else(|| not_found("Function", &name))?;
93        drop(state);
94        let view = stage_view(&f, &stage);
95        let body = render_function_summary(&view, "DescribeFunctionResult");
96        Ok(xml_with_etag(StatusCode::OK, body, &view.etag, None))
97    }
98
99    pub(crate) fn get_function(
100        &self,
101        req: &AwsRequest,
102        route: &Route,
103    ) -> Result<AwsResponse, AwsServiceError> {
104        let name = route_id(route, "Function")?;
105        let stage = parse_stage_query(&req.raw_query);
106        let state = self.state.read();
107        let f = state
108            .accounts
109            .get(DEFAULT_ACCOUNT)
110            .and_then(|a| a.functions.get(&name).cloned())
111            .ok_or_else(|| not_found("Function", &name))?;
112        drop(state);
113        let view = stage_view(&f, &stage);
114        let mut headers = HeaderMap::new();
115        headers.insert(ETAG, view.etag.parse().unwrap());
116        let bytes = base64::engine::general_purpose::STANDARD
117            .decode(view.function_code.as_bytes())
118            .unwrap_or_default();
119        Ok(AwsResponse {
120            status: StatusCode::OK,
121            headers,
122            content_type: "application/octet-stream".to_string(),
123            body: fakecloud_core::service::ResponseBody::Bytes(bytes::Bytes::from(bytes)),
124        })
125    }
126
127    pub(crate) fn update_function(
128        &self,
129        req: &AwsRequest,
130        route: &Route,
131    ) -> Result<AwsResponse, AwsServiceError> {
132        let name = route_id(route, "Function")?;
133        let if_match = require_if_match(req)?;
134        let parsed: UpdateFunctionRequest = xml_io::from_xml_root(&req.body)
135            .map_err(|e| invalid_argument(format!("invalid UpdateFunctionRequest XML: {e}")))?;
136        let mut state = self.state.write();
137        let account = state
138            .accounts
139            .get_mut(DEFAULT_ACCOUNT)
140            .ok_or_else(|| not_found("Function", &name))?;
141        let f = account
142            .functions
143            .get_mut(&name)
144            .ok_or_else(|| not_found("Function", &name))?;
145        if f.etag != if_match {
146            return Err(precondition_failed());
147        }
148        f.config = parsed.function_config;
149        f.function_code = parsed.function_code;
150        f.etag = generate_id_with_prefix("E");
151        f.last_modified_time = Utc::now();
152        f.status = "UNPUBLISHED".to_string();
153        f.stage = "DEVELOPMENT".to_string();
154        let snap = f.clone();
155        drop(state);
156        let body = render_function_summary(&snap, "UpdateFunctionResult");
157        // SDK has a known typo on UpdateFunctionOutput: it deserializes
158        // the etag from header `ETtag`, not `ETag`. Send both so any
159        // SDK version can read it.
160        let mut headers = HeaderMap::new();
161        if let Ok(v) = http::HeaderValue::from_str(&snap.etag) {
162            headers.insert(ETAG, v.clone());
163            headers.insert("ETtag", v);
164        }
165        Ok(xml_response(StatusCode::OK, body, headers))
166    }
167
168    pub(crate) fn delete_function(
169        &self,
170        req: &AwsRequest,
171        route: &Route,
172    ) -> Result<AwsResponse, AwsServiceError> {
173        let name = route_id(route, "Function")?;
174        let if_match = require_if_match(req)?;
175        let mut state = self.state.write();
176        let account = state
177            .accounts
178            .get_mut(DEFAULT_ACCOUNT)
179            .ok_or_else(|| not_found("Function", &name))?;
180        let f = account
181            .functions
182            .get(&name)
183            .ok_or_else(|| not_found("Function", &name))?;
184        if f.etag != if_match {
185            return Err(precondition_failed());
186        }
187        account.functions.remove(&name);
188        drop(state);
189        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
190    }
191
192    pub(crate) fn list_functions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
193        let stage = parse_stage_query(&req.raw_query);
194        let state = self.state.read();
195        let mut items: Vec<StoredFunction> = state
196            .accounts
197            .get(DEFAULT_ACCOUNT)
198            .map(|a| a.functions.values().cloned().collect())
199            .unwrap_or_default();
200        drop(state);
201        items.sort_by(|a, b| a.name.cmp(&b.name));
202
203        let mut body = String::with_capacity(512);
204        body.push_str(XML_DECL);
205        body.push_str(&format!("<FunctionList xmlns=\"{NS}\">"));
206        body.push_str("<Marker></Marker>");
207        body.push_str("<MaxItems>100</MaxItems>");
208        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
209        body.push_str("<Items>");
210        for f in &items {
211            let view = stage_view(f, &stage);
212            body.push_str(&render_function_summary_inner(&view));
213        }
214        body.push_str("</Items>");
215        body.push_str("</FunctionList>");
216        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
217    }
218
219    pub(crate) fn publish_function(
220        &self,
221        req: &AwsRequest,
222        route: &Route,
223    ) -> Result<AwsResponse, AwsServiceError> {
224        let name = route_id(route, "Function")?;
225        let if_match = require_if_match(req)?;
226        let mut state = self.state.write();
227        let account = state
228            .accounts
229            .get_mut(DEFAULT_ACCOUNT)
230            .ok_or_else(|| not_found("Function", &name))?;
231        let f = account
232            .functions
233            .get_mut(&name)
234            .ok_or_else(|| not_found("Function", &name))?;
235        if f.etag != if_match {
236            return Err(precondition_failed());
237        }
238        f.status = "DEPLOYED".to_string();
239        f.stage = "LIVE".to_string();
240        f.last_modified_time = Utc::now();
241        let snap = f.clone();
242        drop(state);
243        let body = render_function_summary(&snap, "PublishFunctionResult");
244        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
245    }
246
247    pub(crate) fn test_function(
248        &self,
249        req: &AwsRequest,
250        route: &Route,
251    ) -> Result<AwsResponse, AwsServiceError> {
252        let name = route_id(route, "Function")?;
253        let if_match = require_if_match(req)?;
254        let state = self.state.read();
255        let f = state
256            .accounts
257            .get(DEFAULT_ACCOUNT)
258            .and_then(|a| a.functions.get(&name).cloned())
259            .ok_or_else(|| not_found("Function", &name))?;
260        drop(state);
261        if f.etag != if_match {
262            return Err(precondition_failed());
263        }
264
265        let mut body = String::with_capacity(512);
266        body.push_str(XML_DECL);
267        body.push_str(&format!("<TestResult xmlns=\"{NS}\">"));
268        body.push_str("<TestFunctionResult>");
269        body.push_str(&format!(
270            "<FunctionSummary>{}</FunctionSummary>",
271            render_function_summary_inner(&f)
272                .replacen("<FunctionSummary>", "", 1)
273                .replacen("</FunctionSummary>", "", 1)
274        ));
275        body.push_str("<ComputeUtilization>0</ComputeUtilization>");
276        body.push_str("<FunctionExecutionLogs></FunctionExecutionLogs>");
277        body.push_str("<FunctionErrorMessage></FunctionErrorMessage>");
278        body.push_str("<FunctionOutput>{}</FunctionOutput>");
279        body.push_str("</TestFunctionResult>");
280        body.push_str("</TestResult>");
281        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
282    }
283}
284
285// ─── Public Keys ──────────────────────────────────────────────────────
286
287impl CloudFrontService {
288    pub(crate) fn create_public_key(
289        &self,
290        req: &AwsRequest,
291    ) -> Result<AwsResponse, AwsServiceError> {
292        let cfg: PublicKeyConfig = xml_io::from_xml_root(&req.body)
293            .map_err(|e| invalid_argument(format!("invalid PublicKeyConfig XML: {e}")))?;
294        if cfg.name.is_empty() {
295            return Err(invalid_argument("PublicKeyConfig.Name is required"));
296        }
297        if cfg.encoded_key.is_empty() {
298            return Err(invalid_argument("PublicKeyConfig.EncodedKey is required"));
299        }
300        let mut state = self.state.write();
301        let account = state
302            .accounts
303            .entry(DEFAULT_ACCOUNT.to_string())
304            .or_default();
305        if let Some(existing) = account
306            .public_keys
307            .values()
308            .find(|p| p.config.caller_reference == cfg.caller_reference)
309        {
310            return Err(aws_error(
311                StatusCode::CONFLICT,
312                "PublicKeyAlreadyExists",
313                format!(
314                    "PublicKey with same CallerReference exists: {}",
315                    existing.id
316                ),
317            ));
318        }
319        let id = generate_id_with_prefix("K");
320        let etag = generate_id_with_prefix("E");
321        let stored = StoredPublicKey {
322            id: id.clone(),
323            etag: etag.clone(),
324            created_time: Utc::now(),
325            config: cfg,
326        };
327        account.public_keys.insert(id.clone(), stored.clone());
328        drop(state);
329        let body = render_public_key(&stored, "PublicKey");
330        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
331    }
332
333    pub(crate) fn get_public_key(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
334        let id = route_id(route, "PublicKey")?;
335        let state = self.state.read();
336        let p = state
337            .accounts
338            .get(DEFAULT_ACCOUNT)
339            .and_then(|a| a.public_keys.get(&id).cloned())
340            .ok_or_else(|| not_found("PublicKey", &id))?;
341        drop(state);
342        let body = render_public_key(&p, "PublicKey");
343        Ok(xml_with_etag(StatusCode::OK, body, &p.etag, None))
344    }
345
346    pub(crate) fn get_public_key_config(
347        &self,
348        route: &Route,
349    ) -> Result<AwsResponse, AwsServiceError> {
350        let id = route_id(route, "PublicKey")?;
351        let state = self.state.read();
352        let p = state
353            .accounts
354            .get(DEFAULT_ACCOUNT)
355            .and_then(|a| a.public_keys.get(&id).cloned())
356            .ok_or_else(|| not_found("PublicKey", &id))?;
357        drop(state);
358        let body = config_xml("PublicKeyConfig", &p.config)?;
359        Ok(xml_with_etag(StatusCode::OK, body, &p.etag, None))
360    }
361
362    pub(crate) fn update_public_key(
363        &self,
364        req: &AwsRequest,
365        route: &Route,
366    ) -> Result<AwsResponse, AwsServiceError> {
367        let id = route_id(route, "PublicKey")?;
368        let if_match = require_if_match(req)?;
369        let cfg: PublicKeyConfig = xml_io::from_xml_root(&req.body)
370            .map_err(|e| invalid_argument(format!("invalid PublicKeyConfig XML: {e}")))?;
371        if cfg.name.is_empty() {
372            return Err(invalid_argument("PublicKeyConfig.Name is required"));
373        }
374        let mut state = self.state.write();
375        let account = state
376            .accounts
377            .get_mut(DEFAULT_ACCOUNT)
378            .ok_or_else(|| not_found("PublicKey", &id))?;
379        let p = account
380            .public_keys
381            .get_mut(&id)
382            .ok_or_else(|| not_found("PublicKey", &id))?;
383        if p.etag != if_match {
384            return Err(precondition_failed());
385        }
386        // CallerReference is immutable per AWS.
387        if p.config.caller_reference != cfg.caller_reference {
388            return Err(invalid_argument(
389                "CallerReference cannot change on UpdatePublicKey",
390            ));
391        }
392        p.config = cfg;
393        p.etag = generate_id_with_prefix("E");
394        let snap = p.clone();
395        drop(state);
396        let body = render_public_key(&snap, "PublicKey");
397        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
398    }
399
400    pub(crate) fn delete_public_key(
401        &self,
402        req: &AwsRequest,
403        route: &Route,
404    ) -> Result<AwsResponse, AwsServiceError> {
405        let id = route_id(route, "PublicKey")?;
406        let if_match = require_if_match(req)?;
407        let mut state = self.state.write();
408        let account = state
409            .accounts
410            .get_mut(DEFAULT_ACCOUNT)
411            .ok_or_else(|| not_found("PublicKey", &id))?;
412        let p = account
413            .public_keys
414            .get(&id)
415            .ok_or_else(|| not_found("PublicKey", &id))?;
416        if p.etag != if_match {
417            return Err(precondition_failed());
418        }
419        account.public_keys.remove(&id);
420        drop(state);
421        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
422    }
423
424    pub(crate) fn list_public_keys(
425        &self,
426        _req: &AwsRequest,
427    ) -> Result<AwsResponse, AwsServiceError> {
428        let state = self.state.read();
429        let mut items: Vec<StoredPublicKey> = state
430            .accounts
431            .get(DEFAULT_ACCOUNT)
432            .map(|a| a.public_keys.values().cloned().collect())
433            .unwrap_or_default();
434        drop(state);
435        items.sort_by(|a, b| a.id.cmp(&b.id));
436
437        let mut body = String::with_capacity(512);
438        body.push_str(XML_DECL);
439        body.push_str(&format!("<PublicKeyList xmlns=\"{NS}\">"));
440        body.push_str("<Marker></Marker>");
441        body.push_str("<MaxItems>100</MaxItems>");
442        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
443        body.push_str("<Items>");
444        for p in &items {
445            body.push_str("<PublicKeySummary>");
446            body.push_str(&format!("<Id>{}</Id>", esc(&p.id)));
447            body.push_str(&format!("<Name>{}</Name>", esc(&p.config.name)));
448            body.push_str(&format!(
449                "<CreatedTime>{}</CreatedTime>",
450                rfc3339(&p.created_time)
451            ));
452            body.push_str(&format!(
453                "<EncodedKey>{}</EncodedKey>",
454                esc(&p.config.encoded_key)
455            ));
456            if let Some(c) = &p.config.comment {
457                body.push_str(&format!("<Comment>{}</Comment>", esc(c)));
458            }
459            body.push_str("</PublicKeySummary>");
460        }
461        body.push_str("</Items>");
462        body.push_str("</PublicKeyList>");
463        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
464    }
465}
466
467// ─── Key Groups ───────────────────────────────────────────────────────
468
469impl CloudFrontService {
470    pub(crate) fn create_key_group(
471        &self,
472        req: &AwsRequest,
473    ) -> Result<AwsResponse, AwsServiceError> {
474        let cfg: KeyGroupConfig = xml_io::from_xml_root(&req.body)
475            .map_err(|e| invalid_argument(format!("invalid KeyGroupConfig XML: {e}")))?;
476        if cfg.name.is_empty() {
477            return Err(invalid_argument("KeyGroupConfig.Name is required"));
478        }
479        let mut state = self.state.write();
480        let account = state
481            .accounts
482            .entry(DEFAULT_ACCOUNT.to_string())
483            .or_default();
484        let id = generate_id_with_prefix("K");
485        let etag = generate_id_with_prefix("E");
486        let stored = StoredKeyGroup {
487            id: id.clone(),
488            etag: etag.clone(),
489            last_modified_time: Utc::now(),
490            config: cfg,
491        };
492        account.key_groups.insert(id.clone(), stored.clone());
493        drop(state);
494        let body = render_key_group(&stored, "KeyGroup");
495        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
496    }
497
498    pub(crate) fn get_key_group(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
499        let id = route_id(route, "KeyGroup")?;
500        let state = self.state.read();
501        let g = state
502            .accounts
503            .get(DEFAULT_ACCOUNT)
504            .and_then(|a| a.key_groups.get(&id).cloned())
505            .ok_or_else(|| not_found("KeyGroup", &id))?;
506        drop(state);
507        let body = render_key_group(&g, "KeyGroup");
508        Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
509    }
510
511    pub(crate) fn get_key_group_config(
512        &self,
513        route: &Route,
514    ) -> Result<AwsResponse, AwsServiceError> {
515        let id = route_id(route, "KeyGroup")?;
516        let state = self.state.read();
517        let g = state
518            .accounts
519            .get(DEFAULT_ACCOUNT)
520            .and_then(|a| a.key_groups.get(&id).cloned())
521            .ok_or_else(|| not_found("KeyGroup", &id))?;
522        drop(state);
523        let body = config_xml("KeyGroupConfig", &g.config)?;
524        Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
525    }
526
527    pub(crate) fn update_key_group(
528        &self,
529        req: &AwsRequest,
530        route: &Route,
531    ) -> Result<AwsResponse, AwsServiceError> {
532        let id = route_id(route, "KeyGroup")?;
533        let if_match = require_if_match(req)?;
534        let cfg: KeyGroupConfig = xml_io::from_xml_root(&req.body)
535            .map_err(|e| invalid_argument(format!("invalid KeyGroupConfig XML: {e}")))?;
536        if cfg.name.is_empty() {
537            return Err(invalid_argument("KeyGroupConfig.Name is required"));
538        }
539        let mut state = self.state.write();
540        let account = state
541            .accounts
542            .get_mut(DEFAULT_ACCOUNT)
543            .ok_or_else(|| not_found("KeyGroup", &id))?;
544        let g = account
545            .key_groups
546            .get_mut(&id)
547            .ok_or_else(|| not_found("KeyGroup", &id))?;
548        if g.etag != if_match {
549            return Err(precondition_failed());
550        }
551        g.config = cfg;
552        g.etag = generate_id_with_prefix("E");
553        g.last_modified_time = Utc::now();
554        let snap = g.clone();
555        drop(state);
556        let body = render_key_group(&snap, "KeyGroup");
557        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
558    }
559
560    pub(crate) fn delete_key_group(
561        &self,
562        req: &AwsRequest,
563        route: &Route,
564    ) -> Result<AwsResponse, AwsServiceError> {
565        let id = route_id(route, "KeyGroup")?;
566        let if_match = require_if_match(req)?;
567        let mut state = self.state.write();
568        let account = state
569            .accounts
570            .get_mut(DEFAULT_ACCOUNT)
571            .ok_or_else(|| not_found("KeyGroup", &id))?;
572        let g = account
573            .key_groups
574            .get(&id)
575            .ok_or_else(|| not_found("KeyGroup", &id))?;
576        if g.etag != if_match {
577            return Err(precondition_failed());
578        }
579        account.key_groups.remove(&id);
580        drop(state);
581        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
582    }
583
584    pub(crate) fn list_key_groups(
585        &self,
586        _req: &AwsRequest,
587    ) -> Result<AwsResponse, AwsServiceError> {
588        let state = self.state.read();
589        let mut items: Vec<StoredKeyGroup> = state
590            .accounts
591            .get(DEFAULT_ACCOUNT)
592            .map(|a| a.key_groups.values().cloned().collect())
593            .unwrap_or_default();
594        drop(state);
595        items.sort_by(|a, b| a.config.name.cmp(&b.config.name));
596
597        let mut body = String::with_capacity(512);
598        body.push_str(XML_DECL);
599        body.push_str(&format!("<KeyGroupList xmlns=\"{NS}\">"));
600        body.push_str("<Marker></Marker>");
601        body.push_str("<MaxItems>100</MaxItems>");
602        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
603        body.push_str("<Items>");
604        for g in &items {
605            body.push_str("<KeyGroupSummary>");
606            body.push_str("<KeyGroup>");
607            push_key_group_inner(&mut body, g);
608            body.push_str("</KeyGroup>");
609            body.push_str("</KeyGroupSummary>");
610        }
611        body.push_str("</Items>");
612        body.push_str("</KeyGroupList>");
613        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
614    }
615}
616
617// ─── Key Value Stores ─────────────────────────────────────────────────
618
619impl CloudFrontService {
620    pub(crate) fn create_key_value_store(
621        &self,
622        req: &AwsRequest,
623    ) -> Result<AwsResponse, AwsServiceError> {
624        let parsed: CreateKeyValueStoreRequest = xml_io::from_xml_root(&req.body)
625            .map_err(|e| invalid_argument(format!("invalid CreateKeyValueStore XML: {e}")))?;
626        if parsed.name.is_empty() {
627            return Err(invalid_argument("Name is required"));
628        }
629        let mut state = self.state.write();
630        let account = state
631            .accounts
632            .entry(DEFAULT_ACCOUNT.to_string())
633            .or_default();
634        if account.key_value_stores.contains_key(&parsed.name) {
635            return Err(aws_error(
636                StatusCode::CONFLICT,
637                "EntityAlreadyExists",
638                format!("KeyValueStore {} already exists", parsed.name),
639            ));
640        }
641        let now = Utc::now();
642        let id = Uuid::new_v4().to_string();
643        let etag = generate_id_with_prefix("E");
644        let arn = format!(
645            "arn:aws:cloudfront::{}:key-value-store/{}",
646            DEFAULT_ACCOUNT, id
647        );
648        let stored = StoredKeyValueStore {
649            name: parsed.name.clone(),
650            id,
651            etag: etag.clone(),
652            arn,
653            status: "READY".to_string(),
654            created_time: now,
655            last_modified_time: now,
656            comment: parsed.comment,
657            import_source: parsed.import_source,
658        };
659        account
660            .key_value_stores
661            .insert(parsed.name.clone(), stored.clone());
662        drop(state);
663        let body = render_key_value_store(&stored, "CreateKeyValueStoreResult");
664        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, None))
665    }
666
667    pub(crate) fn describe_key_value_store(
668        &self,
669        route: &Route,
670    ) -> Result<AwsResponse, AwsServiceError> {
671        let name = route_id(route, "KeyValueStore")?;
672        let state = self.state.read();
673        let kvs = state
674            .accounts
675            .get(DEFAULT_ACCOUNT)
676            .and_then(|a| a.key_value_stores.get(&name).cloned())
677            .ok_or_else(|| not_found("KeyValueStore", &name))?;
678        drop(state);
679        let body = render_key_value_store(&kvs, "DescribeKeyValueStoreResult");
680        Ok(xml_with_etag(StatusCode::OK, body, &kvs.etag, None))
681    }
682
683    pub(crate) fn update_key_value_store(
684        &self,
685        req: &AwsRequest,
686        route: &Route,
687    ) -> Result<AwsResponse, AwsServiceError> {
688        let name = route_id(route, "KeyValueStore")?;
689        let if_match = require_if_match(req)?;
690        let parsed: UpdateKeyValueStoreRequest = xml_io::from_xml_root(&req.body)
691            .map_err(|e| invalid_argument(format!("invalid UpdateKeyValueStore XML: {e}")))?;
692        let mut state = self.state.write();
693        let account = state
694            .accounts
695            .get_mut(DEFAULT_ACCOUNT)
696            .ok_or_else(|| not_found("KeyValueStore", &name))?;
697        let kvs = account
698            .key_value_stores
699            .get_mut(&name)
700            .ok_or_else(|| not_found("KeyValueStore", &name))?;
701        if kvs.etag != if_match {
702            return Err(precondition_failed());
703        }
704        kvs.comment = Some(parsed.comment);
705        kvs.etag = generate_id_with_prefix("E");
706        kvs.last_modified_time = Utc::now();
707        let snap = kvs.clone();
708        drop(state);
709        let body = render_key_value_store(&snap, "UpdateKeyValueStoreResult");
710        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
711    }
712
713    pub(crate) fn delete_key_value_store(
714        &self,
715        req: &AwsRequest,
716        route: &Route,
717    ) -> Result<AwsResponse, AwsServiceError> {
718        let name = route_id(route, "KeyValueStore")?;
719        let if_match = require_if_match(req)?;
720        let mut state = self.state.write();
721        let account = state
722            .accounts
723            .get_mut(DEFAULT_ACCOUNT)
724            .ok_or_else(|| not_found("KeyValueStore", &name))?;
725        let kvs = account
726            .key_value_stores
727            .get(&name)
728            .ok_or_else(|| not_found("KeyValueStore", &name))?;
729        if kvs.etag != if_match {
730            return Err(precondition_failed());
731        }
732        account.key_value_stores.remove(&name);
733        drop(state);
734        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
735    }
736
737    pub(crate) fn list_key_value_stores(
738        &self,
739        _req: &AwsRequest,
740    ) -> Result<AwsResponse, AwsServiceError> {
741        let state = self.state.read();
742        let mut items: Vec<StoredKeyValueStore> = state
743            .accounts
744            .get(DEFAULT_ACCOUNT)
745            .map(|a| a.key_value_stores.values().cloned().collect())
746            .unwrap_or_default();
747        drop(state);
748        items.sort_by(|a, b| a.name.cmp(&b.name));
749
750        let mut body = String::with_capacity(512);
751        body.push_str(XML_DECL);
752        body.push_str(&format!("<KeyValueStoreList xmlns=\"{NS}\">"));
753        body.push_str("<NextMarker></NextMarker>");
754        body.push_str("<MaxItems>100</MaxItems>");
755        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
756        body.push_str("<Items>");
757        for kvs in &items {
758            body.push_str("<KeyValueStore>");
759            push_kvs_inner(&mut body, kvs);
760            body.push_str("</KeyValueStore>");
761        }
762        body.push_str("</Items>");
763        body.push_str("</KeyValueStoreList>");
764        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
765    }
766}
767
768// ─── Origin Access Identities (legacy) ────────────────────────────────
769
770impl CloudFrontService {
771    pub(crate) fn create_oai(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
772        let cfg: CloudFrontOriginAccessIdentityConfig = xml_io::from_xml_root(&req.body)
773            .map_err(|e| invalid_argument(format!("invalid OAI config XML: {e}")))?;
774        if cfg.caller_reference.is_empty() {
775            return Err(invalid_argument("CallerReference is required"));
776        }
777        let mut state = self.state.write();
778        let account = state
779            .accounts
780            .entry(DEFAULT_ACCOUNT.to_string())
781            .or_default();
782        if let Some(existing) = account
783            .origin_access_identities
784            .values()
785            .find(|o| o.config.caller_reference == cfg.caller_reference)
786        {
787            return Err(aws_error(
788                StatusCode::CONFLICT,
789                "CloudFrontOriginAccessIdentityAlreadyExists",
790                format!("OAI with same CallerReference exists: {}", existing.id),
791            ));
792        }
793        let id = format!(
794            "E{}",
795            Uuid::new_v4()
796                .simple()
797                .to_string()
798                .to_uppercase()
799                .chars()
800                .take(13)
801                .collect::<String>()
802        );
803        let etag = generate_id_with_prefix("E");
804        let canonical = Uuid::new_v4().simple().to_string();
805        let stored = StoredOriginAccessIdentity {
806            id: id.clone(),
807            etag: etag.clone(),
808            s3_canonical_user_id: canonical,
809            config: cfg,
810        };
811        account
812            .origin_access_identities
813            .insert(id.clone(), stored.clone());
814        drop(state);
815        let body = render_oai(&stored, "CloudFrontOriginAccessIdentity");
816        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
817    }
818
819    pub(crate) fn get_oai(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
820        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
821        let state = self.state.read();
822        let oai = state
823            .accounts
824            .get(DEFAULT_ACCOUNT)
825            .and_then(|a| a.origin_access_identities.get(&id).cloned())
826            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
827        drop(state);
828        let body = render_oai(&oai, "CloudFrontOriginAccessIdentity");
829        Ok(xml_with_etag(StatusCode::OK, body, &oai.etag, None))
830    }
831
832    pub(crate) fn get_oai_config(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
833        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
834        let state = self.state.read();
835        let oai = state
836            .accounts
837            .get(DEFAULT_ACCOUNT)
838            .and_then(|a| a.origin_access_identities.get(&id).cloned())
839            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
840        drop(state);
841        let body = config_xml("CloudFrontOriginAccessIdentityConfig", &oai.config)?;
842        Ok(xml_with_etag(StatusCode::OK, body, &oai.etag, None))
843    }
844
845    pub(crate) fn update_oai(
846        &self,
847        req: &AwsRequest,
848        route: &Route,
849    ) -> Result<AwsResponse, AwsServiceError> {
850        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
851        let if_match = require_if_match(req)?;
852        let cfg: CloudFrontOriginAccessIdentityConfig = xml_io::from_xml_root(&req.body)
853            .map_err(|e| invalid_argument(format!("invalid OAI config XML: {e}")))?;
854        let mut state = self.state.write();
855        let account = state
856            .accounts
857            .get_mut(DEFAULT_ACCOUNT)
858            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
859        let oai = account
860            .origin_access_identities
861            .get_mut(&id)
862            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
863        if oai.etag != if_match {
864            return Err(precondition_failed());
865        }
866        if oai.config.caller_reference != cfg.caller_reference {
867            return Err(invalid_argument(
868                "CallerReference cannot change on UpdateCloudFrontOriginAccessIdentity",
869            ));
870        }
871        oai.config = cfg;
872        oai.etag = generate_id_with_prefix("E");
873        let snap = oai.clone();
874        drop(state);
875        let body = render_oai(&snap, "CloudFrontOriginAccessIdentity");
876        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
877    }
878
879    pub(crate) fn delete_oai(
880        &self,
881        req: &AwsRequest,
882        route: &Route,
883    ) -> Result<AwsResponse, AwsServiceError> {
884        let id = route_id(route, "CloudFrontOriginAccessIdentity")?;
885        let if_match = require_if_match(req)?;
886        let mut state = self.state.write();
887        let account = state
888            .accounts
889            .get_mut(DEFAULT_ACCOUNT)
890            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
891        let oai = account
892            .origin_access_identities
893            .get(&id)
894            .ok_or_else(|| not_found("CloudFrontOriginAccessIdentity", &id))?;
895        if oai.etag != if_match {
896            return Err(precondition_failed());
897        }
898        account.origin_access_identities.remove(&id);
899        drop(state);
900        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
901    }
902
903    pub(crate) fn list_oai(&self, _req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
904        let state = self.state.read();
905        let mut items: Vec<StoredOriginAccessIdentity> = state
906            .accounts
907            .get(DEFAULT_ACCOUNT)
908            .map(|a| a.origin_access_identities.values().cloned().collect())
909            .unwrap_or_default();
910        drop(state);
911        items.sort_by(|a, b| a.id.cmp(&b.id));
912
913        let mut body = String::with_capacity(512);
914        body.push_str(XML_DECL);
915        body.push_str(&format!(
916            "<CloudFrontOriginAccessIdentityList xmlns=\"{NS}\">"
917        ));
918        body.push_str("<Marker></Marker>");
919        body.push_str("<MaxItems>100</MaxItems>");
920        body.push_str("<IsTruncated>false</IsTruncated>");
921        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
922        body.push_str("<Items>");
923        for oai in &items {
924            body.push_str("<CloudFrontOriginAccessIdentitySummary>");
925            body.push_str(&format!("<Id>{}</Id>", esc(&oai.id)));
926            body.push_str(&format!(
927                "<S3CanonicalUserId>{}</S3CanonicalUserId>",
928                esc(&oai.s3_canonical_user_id)
929            ));
930            body.push_str(&format!("<Comment>{}</Comment>", esc(&oai.config.comment)));
931            body.push_str("</CloudFrontOriginAccessIdentitySummary>");
932        }
933        body.push_str("</Items>");
934        body.push_str("</CloudFrontOriginAccessIdentityList>");
935        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
936    }
937}
938
939// ─── Monitoring Subscriptions ─────────────────────────────────────────
940
941impl CloudFrontService {
942    pub(crate) fn create_monitoring_subscription(
943        &self,
944        req: &AwsRequest,
945        route: &Route,
946    ) -> Result<AwsResponse, AwsServiceError> {
947        let dist_id = route_id(route, "Distribution")?;
948        let parsed: MonitoringSubscriptionBody = xml_io::from_xml_root(&req.body)
949            .map_err(|e| invalid_argument(format!("invalid MonitoringSubscription XML: {e}")))?;
950        let mut state = self.state.write();
951        let account = state
952            .accounts
953            .entry(DEFAULT_ACCOUNT.to_string())
954            .or_default();
955        if !account.distributions.contains_key(&dist_id) {
956            return Err(not_found("Distribution", &dist_id));
957        }
958        let stored = StoredMonitoringSubscription {
959            distribution_id: dist_id.clone(),
960            config: parsed.realtime_metrics_subscription_config,
961        };
962        account
963            .monitoring_subscriptions
964            .insert(dist_id.clone(), stored.clone());
965        drop(state);
966        let body = render_monitoring(&stored);
967        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
968    }
969
970    pub(crate) fn get_monitoring_subscription(
971        &self,
972        route: &Route,
973    ) -> Result<AwsResponse, AwsServiceError> {
974        let dist_id = route_id(route, "Distribution")?;
975        let state = self.state.read();
976        let m = state
977            .accounts
978            .get(DEFAULT_ACCOUNT)
979            .and_then(|a| a.monitoring_subscriptions.get(&dist_id).cloned())
980            .ok_or_else(|| {
981                aws_error(
982                    StatusCode::NOT_FOUND,
983                    "NoSuchMonitoringSubscription",
984                    format!("No monitoring subscription for distribution {dist_id}"),
985                )
986            })?;
987        drop(state);
988        let body = render_monitoring(&m);
989        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
990    }
991
992    pub(crate) fn delete_monitoring_subscription(
993        &self,
994        route: &Route,
995    ) -> Result<AwsResponse, AwsServiceError> {
996        let dist_id = route_id(route, "Distribution")?;
997        let mut state = self.state.write();
998        let account = state
999            .accounts
1000            .get_mut(DEFAULT_ACCOUNT)
1001            .ok_or_else(|| not_found("Distribution", &dist_id))?;
1002        if account.monitoring_subscriptions.remove(&dist_id).is_none() {
1003            return Err(aws_error(
1004                StatusCode::NOT_FOUND,
1005                "NoSuchMonitoringSubscription",
1006                format!("No monitoring subscription for distribution {dist_id}"),
1007            ));
1008        }
1009        drop(state);
1010        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
1011    }
1012}
1013
1014// ─── Helpers ──────────────────────────────────────────────────────────
1015
1016#[derive(Debug, serde::Deserialize)]
1017#[serde(rename_all = "PascalCase")]
1018struct CreateFunctionRequest {
1019    name: String,
1020    function_config: FunctionConfig,
1021    /// Base64-encoded source.
1022    function_code: String,
1023}
1024
1025#[derive(Debug, serde::Deserialize)]
1026#[serde(rename_all = "PascalCase")]
1027struct UpdateFunctionRequest {
1028    function_config: FunctionConfig,
1029    function_code: String,
1030}
1031
1032#[derive(Debug, serde::Deserialize)]
1033#[serde(rename_all = "PascalCase")]
1034struct CreateKeyValueStoreRequest {
1035    name: String,
1036    #[serde(default)]
1037    comment: Option<String>,
1038    #[serde(default)]
1039    import_source: Option<ImportSource>,
1040}
1041
1042#[derive(Debug, serde::Deserialize)]
1043#[serde(rename_all = "PascalCase")]
1044struct UpdateKeyValueStoreRequest {
1045    comment: String,
1046}
1047
1048fn config_xml<T: serde::Serialize>(root: &str, cfg: &T) -> Result<String, AwsServiceError> {
1049    let inner = quick_xml::se::to_string_with_root(root, cfg).map_err(|e| {
1050        aws_error(
1051            StatusCode::INTERNAL_SERVER_ERROR,
1052            "InternalError",
1053            format!("xml encode failed: {e}"),
1054        )
1055    })?;
1056    let stamped = inner.replacen(
1057        &format!("<{root}>"),
1058        &format!("<{root} xmlns=\"{NS}\">", NS = crate::NAMESPACE),
1059        1,
1060    );
1061    Ok(format!("{XML_DECL}{stamped}"))
1062}
1063
1064fn parse_stage_query(query: &str) -> Option<String> {
1065    use std::collections::HashMap;
1066    let pairs: HashMap<&str, &str> = query.split('&').filter_map(|p| p.split_once('=')).collect();
1067    pairs.get("Stage").map(|s| s.to_string())
1068}
1069
1070fn stage_view(f: &StoredFunction, stage: &Option<String>) -> StoredFunction {
1071    let mut clone = f.clone();
1072    if stage.as_deref() == Some("LIVE") {
1073        clone.stage = "LIVE".into();
1074    }
1075    clone
1076}
1077
1078fn render_function_summary(f: &StoredFunction, _root: &str) -> String {
1079    // CloudFront returns FunctionSummary as the root for Create/Describe/
1080    // Update/Publish — there is no operation-specific wrapper element.
1081    let mut out = String::with_capacity(512);
1082    out.push_str(XML_DECL);
1083    out.push_str(&render_function_summary_inner_with_ns(f));
1084    out
1085}
1086
1087fn render_function_summary_inner_with_ns(f: &StoredFunction) -> String {
1088    let mut out = String::with_capacity(512);
1089    out.push_str(&format!("<FunctionSummary xmlns=\"{NS}\">"));
1090    out.push_str(&render_function_summary_body(f));
1091    out.push_str("</FunctionSummary>");
1092    out
1093}
1094
1095fn render_function_summary_inner(f: &StoredFunction) -> String {
1096    let mut out = String::with_capacity(512);
1097    out.push_str("<FunctionSummary>");
1098    out.push_str(&render_function_summary_body(f));
1099    out.push_str("</FunctionSummary>");
1100    out
1101}
1102
1103fn render_function_summary_body(f: &StoredFunction) -> String {
1104    let mut out = String::with_capacity(512);
1105    out.push_str(&format!("<Name>{}</Name>", esc(&f.name)));
1106    out.push_str(&format!("<Status>{}</Status>", esc(&f.status)));
1107    out.push_str("<FunctionConfig>");
1108    if let Some(c) = &f.config.comment {
1109        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
1110    } else {
1111        out.push_str("<Comment></Comment>");
1112    }
1113    out.push_str(&format!("<Runtime>{}</Runtime>", esc(&f.config.runtime)));
1114    out.push_str("</FunctionConfig>");
1115    out.push_str("<FunctionMetadata>");
1116    out.push_str(&format!(
1117        "<FunctionARN>{}</FunctionARN>",
1118        esc(&f.function_arn)
1119    ));
1120    out.push_str(&format!("<Stage>{}</Stage>", esc(&f.stage)));
1121    out.push_str(&format!(
1122        "<CreatedTime>{}</CreatedTime>",
1123        rfc3339(&f.created_time)
1124    ));
1125    out.push_str(&format!(
1126        "<LastModifiedTime>{}</LastModifiedTime>",
1127        rfc3339(&f.last_modified_time)
1128    ));
1129    out.push_str("</FunctionMetadata>");
1130    out
1131}
1132
1133fn render_public_key(p: &StoredPublicKey, root: &str) -> String {
1134    let mut out = String::with_capacity(512);
1135    out.push_str(XML_DECL);
1136    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
1137    out.push_str(&format!("<Id>{}</Id>", esc(&p.id)));
1138    out.push_str(&format!(
1139        "<CreatedTime>{}</CreatedTime>",
1140        rfc3339(&p.created_time)
1141    ));
1142    out.push_str("<PublicKeyConfig>");
1143    out.push_str(&format!(
1144        "<CallerReference>{}</CallerReference>",
1145        esc(&p.config.caller_reference)
1146    ));
1147    out.push_str(&format!("<Name>{}</Name>", esc(&p.config.name)));
1148    out.push_str(&format!(
1149        "<EncodedKey>{}</EncodedKey>",
1150        esc(&p.config.encoded_key)
1151    ));
1152    if let Some(c) = &p.config.comment {
1153        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
1154    }
1155    out.push_str("</PublicKeyConfig>");
1156    out.push_str(&format!("</{root}>"));
1157    out
1158}
1159
1160fn push_key_group_inner(out: &mut String, g: &StoredKeyGroup) {
1161    out.push_str(&format!("<Id>{}</Id>", esc(&g.id)));
1162    out.push_str(&format!(
1163        "<LastModifiedTime>{}</LastModifiedTime>",
1164        rfc3339(&g.last_modified_time)
1165    ));
1166    out.push_str("<KeyGroupConfig>");
1167    out.push_str(&format!("<Name>{}</Name>", esc(&g.config.name)));
1168    out.push_str("<Items>");
1169    for k in &g.config.items.public_key {
1170        out.push_str(&format!("<PublicKey>{}</PublicKey>", esc(k)));
1171    }
1172    out.push_str("</Items>");
1173    if let Some(c) = &g.config.comment {
1174        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
1175    }
1176    out.push_str("</KeyGroupConfig>");
1177}
1178
1179fn render_key_group(g: &StoredKeyGroup, root: &str) -> String {
1180    let mut out = String::with_capacity(512);
1181    out.push_str(XML_DECL);
1182    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
1183    push_key_group_inner(&mut out, g);
1184    out.push_str(&format!("</{root}>"));
1185    out
1186}
1187
1188fn push_kvs_inner(out: &mut String, kvs: &StoredKeyValueStore) {
1189    out.push_str(&format!("<Name>{}</Name>", esc(&kvs.name)));
1190    out.push_str(&format!("<Id>{}</Id>", esc(&kvs.id)));
1191    out.push_str(&format!(
1192        "<Comment>{}</Comment>",
1193        esc(kvs.comment.as_deref().unwrap_or(""))
1194    ));
1195    out.push_str(&format!("<ARN>{}</ARN>", esc(&kvs.arn)));
1196    out.push_str(&format!("<Status>{}</Status>", esc(&kvs.status)));
1197    out.push_str(&format!(
1198        "<LastModifiedTime>{}</LastModifiedTime>",
1199        rfc3339(&kvs.last_modified_time)
1200    ));
1201}
1202
1203fn render_key_value_store(kvs: &StoredKeyValueStore, _root: &str) -> String {
1204    // SDK expects KeyValueStore as root for Create/Describe/Update.
1205    let mut out = String::with_capacity(512);
1206    out.push_str(XML_DECL);
1207    out.push_str(&format!("<KeyValueStore xmlns=\"{NS}\">"));
1208    push_kvs_inner(&mut out, kvs);
1209    out.push_str("</KeyValueStore>");
1210    out
1211}
1212
1213fn render_oai(oai: &StoredOriginAccessIdentity, root: &str) -> String {
1214    let mut out = String::with_capacity(512);
1215    out.push_str(XML_DECL);
1216    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
1217    out.push_str(&format!("<Id>{}</Id>", esc(&oai.id)));
1218    out.push_str(&format!(
1219        "<S3CanonicalUserId>{}</S3CanonicalUserId>",
1220        esc(&oai.s3_canonical_user_id)
1221    ));
1222    out.push_str("<CloudFrontOriginAccessIdentityConfig>");
1223    out.push_str(&format!(
1224        "<CallerReference>{}</CallerReference>",
1225        esc(&oai.config.caller_reference)
1226    ));
1227    out.push_str(&format!("<Comment>{}</Comment>", esc(&oai.config.comment)));
1228    out.push_str("</CloudFrontOriginAccessIdentityConfig>");
1229    out.push_str(&format!("</{root}>"));
1230    out
1231}
1232
1233fn render_monitoring(m: &StoredMonitoringSubscription) -> String {
1234    let mut out = String::with_capacity(256);
1235    out.push_str(XML_DECL);
1236    out.push_str(&format!("<MonitoringSubscription xmlns=\"{NS}\">"));
1237    out.push_str("<RealtimeMetricsSubscriptionConfig>");
1238    out.push_str(&format!(
1239        "<RealtimeMetricsSubscriptionStatus>{}</RealtimeMetricsSubscriptionStatus>",
1240        esc(&m.config.realtime_metrics_subscription_status)
1241    ));
1242    out.push_str("</RealtimeMetricsSubscriptionConfig>");
1243    out.push_str("</MonitoringSubscription>");
1244    out
1245}