Skip to main content

fakecloud_cloudfront/
fle_service.rs

1//! Handlers for CloudFront Field-Level Encryption (configs + profiles)
2//! and Realtime Log Configs.
3
4use chrono::Utc;
5use http::{HeaderMap, StatusCode};
6
7use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
8
9use crate::fle::{
10    CreateRealtimeLogConfigRequest, FieldLevelEncryptionConfig, FieldLevelEncryptionProfileConfig,
11    GetOrDeleteRealtimeLogConfigRequest, StoredFieldLevelEncryption,
12    StoredFieldLevelEncryptionProfile, StoredRealtimeLogConfig, UpdateRealtimeLogConfigRequest,
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// ─── Field-Level Encryption Config ────────────────────────────────────
28
29impl CloudFrontService {
30    pub(crate) fn create_field_level_encryption_config(
31        &self,
32        req: &AwsRequest,
33    ) -> Result<AwsResponse, AwsServiceError> {
34        let cfg: FieldLevelEncryptionConfig = xml_io::from_xml_root(&req.body).map_err(|e| {
35            invalid_argument(format!("invalid FieldLevelEncryptionConfig XML: {e}"))
36        })?;
37        if cfg.caller_reference.is_empty() {
38            return Err(invalid_argument("CallerReference is required"));
39        }
40        let mut state = self.state.write();
41        let account = state
42            .accounts
43            .entry(DEFAULT_ACCOUNT.to_string())
44            .or_default();
45        if let Some(existing) = account
46            .field_level_encryptions
47            .values()
48            .find(|f| f.config.caller_reference == cfg.caller_reference)
49        {
50            return Err(aws_error(
51                StatusCode::CONFLICT,
52                "FieldLevelEncryptionConfigAlreadyExists",
53                format!(
54                    "FieldLevelEncryption with same CallerReference exists: {}",
55                    existing.id
56                ),
57            ));
58        }
59        let id = generate_id_with_prefix("F");
60        let etag = generate_id_with_prefix("E");
61        let stored = StoredFieldLevelEncryption {
62            id: id.clone(),
63            etag: etag.clone(),
64            last_modified_time: Utc::now(),
65            config: cfg,
66        };
67        account
68            .field_level_encryptions
69            .insert(id.clone(), stored.clone());
70        drop(state);
71        let body = render_fle(&stored);
72        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
73    }
74
75    pub(crate) fn get_field_level_encryption(
76        &self,
77        route: &Route,
78    ) -> Result<AwsResponse, AwsServiceError> {
79        let id = route_id(route, "FieldLevelEncryption")?;
80        let state = self.state.read();
81        let f = state
82            .accounts
83            .get(DEFAULT_ACCOUNT)
84            .and_then(|a| a.field_level_encryptions.get(&id).cloned())
85            .ok_or_else(|| not_found("FieldLevelEncryption", &id))?;
86        drop(state);
87        let body = render_fle(&f);
88        Ok(xml_with_etag(StatusCode::OK, body, &f.etag, None))
89    }
90
91    pub(crate) fn get_field_level_encryption_config(
92        &self,
93        route: &Route,
94    ) -> Result<AwsResponse, AwsServiceError> {
95        let id = route_id(route, "FieldLevelEncryption")?;
96        let state = self.state.read();
97        let f = state
98            .accounts
99            .get(DEFAULT_ACCOUNT)
100            .and_then(|a| a.field_level_encryptions.get(&id).cloned())
101            .ok_or_else(|| not_found("FieldLevelEncryption", &id))?;
102        drop(state);
103        let body = render_fle_config(&f.config);
104        Ok(xml_with_etag(StatusCode::OK, body, &f.etag, None))
105    }
106
107    pub(crate) fn update_field_level_encryption_config(
108        &self,
109        req: &AwsRequest,
110        route: &Route,
111    ) -> Result<AwsResponse, AwsServiceError> {
112        let id = route_id(route, "FieldLevelEncryption")?;
113        let if_match = require_if_match(req)?;
114        let cfg: FieldLevelEncryptionConfig = xml_io::from_xml_root(&req.body).map_err(|e| {
115            invalid_argument(format!("invalid FieldLevelEncryptionConfig XML: {e}"))
116        })?;
117        let mut state = self.state.write();
118        let account = state
119            .accounts
120            .get_mut(DEFAULT_ACCOUNT)
121            .ok_or_else(|| not_found("FieldLevelEncryption", &id))?;
122        let f = account
123            .field_level_encryptions
124            .get_mut(&id)
125            .ok_or_else(|| not_found("FieldLevelEncryption", &id))?;
126        if f.etag != if_match {
127            return Err(precondition_failed());
128        }
129        if f.config.caller_reference != cfg.caller_reference {
130            return Err(invalid_argument(
131                "CallerReference cannot change on UpdateFieldLevelEncryptionConfig",
132            ));
133        }
134        f.config = cfg;
135        f.etag = generate_id_with_prefix("E");
136        f.last_modified_time = Utc::now();
137        let snap = f.clone();
138        drop(state);
139        let body = render_fle(&snap);
140        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
141    }
142
143    pub(crate) fn delete_field_level_encryption_config(
144        &self,
145        req: &AwsRequest,
146        route: &Route,
147    ) -> Result<AwsResponse, AwsServiceError> {
148        let id = route_id(route, "FieldLevelEncryption")?;
149        let if_match = require_if_match(req)?;
150        let mut state = self.state.write();
151        let account = state
152            .accounts
153            .get_mut(DEFAULT_ACCOUNT)
154            .ok_or_else(|| not_found("FieldLevelEncryption", &id))?;
155        let f = account
156            .field_level_encryptions
157            .get(&id)
158            .ok_or_else(|| not_found("FieldLevelEncryption", &id))?;
159        if f.etag != if_match {
160            return Err(precondition_failed());
161        }
162        account.field_level_encryptions.remove(&id);
163        drop(state);
164        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
165    }
166
167    pub(crate) fn list_field_level_encryption_configs(
168        &self,
169        _req: &AwsRequest,
170    ) -> Result<AwsResponse, AwsServiceError> {
171        let state = self.state.read();
172        let mut items: Vec<StoredFieldLevelEncryption> = state
173            .accounts
174            .get(DEFAULT_ACCOUNT)
175            .map(|a| a.field_level_encryptions.values().cloned().collect())
176            .unwrap_or_default();
177        drop(state);
178        items.sort_by(|a, b| a.id.cmp(&b.id));
179
180        let mut body = String::with_capacity(512);
181        body.push_str(XML_DECL);
182        body.push_str(&format!("<FieldLevelEncryptionList xmlns=\"{NS}\">"));
183        body.push_str("<NextMarker></NextMarker>");
184        body.push_str("<MaxItems>100</MaxItems>");
185        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
186        body.push_str("<Items>");
187        for f in &items {
188            body.push_str("<FieldLevelEncryptionSummary>");
189            body.push_str(&format!("<Id>{}</Id>", esc(&f.id)));
190            body.push_str(&format!(
191                "<LastModifiedTime>{}</LastModifiedTime>",
192                rfc3339(&f.last_modified_time)
193            ));
194            if let Some(c) = &f.config.comment {
195                body.push_str(&format!("<Comment>{}</Comment>", esc(c)));
196            }
197            body.push_str(&render_query_arg_profile_config(
198                &f.config.query_arg_profile_config,
199            ));
200            body.push_str(&render_content_type_profile_config(
201                &f.config.content_type_profile_config,
202            ));
203            body.push_str("</FieldLevelEncryptionSummary>");
204        }
205        body.push_str("</Items>");
206        body.push_str("</FieldLevelEncryptionList>");
207        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
208    }
209}
210
211// ─── Field-Level Encryption Profile ───────────────────────────────────
212
213impl CloudFrontService {
214    pub(crate) fn create_field_level_encryption_profile(
215        &self,
216        req: &AwsRequest,
217    ) -> Result<AwsResponse, AwsServiceError> {
218        let cfg: FieldLevelEncryptionProfileConfig =
219            xml_io::from_xml_root(&req.body).map_err(|e| {
220                invalid_argument(format!(
221                    "invalid FieldLevelEncryptionProfileConfig XML: {e}"
222                ))
223            })?;
224        if cfg.name.is_empty() {
225            return Err(invalid_argument(
226                "FieldLevelEncryptionProfileConfig.Name is required",
227            ));
228        }
229        if cfg.caller_reference.is_empty() {
230            return Err(invalid_argument("CallerReference is required"));
231        }
232        let mut state = self.state.write();
233        let account = state
234            .accounts
235            .entry(DEFAULT_ACCOUNT.to_string())
236            .or_default();
237        if let Some(existing) = account
238            .field_level_encryption_profiles
239            .values()
240            .find(|p| p.config.caller_reference == cfg.caller_reference)
241        {
242            return Err(aws_error(
243                StatusCode::CONFLICT,
244                "FieldLevelEncryptionProfileAlreadyExists",
245                format!(
246                    "FieldLevelEncryptionProfile with same CallerReference exists: {}",
247                    existing.id
248                ),
249            ));
250        }
251        let id = generate_id_with_prefix("FP");
252        let etag = generate_id_with_prefix("E");
253        let stored = StoredFieldLevelEncryptionProfile {
254            id: id.clone(),
255            etag: etag.clone(),
256            last_modified_time: Utc::now(),
257            config: cfg,
258        };
259        account
260            .field_level_encryption_profiles
261            .insert(id.clone(), stored.clone());
262        drop(state);
263        let body = render_fle_profile(&stored);
264        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
265    }
266
267    pub(crate) fn get_field_level_encryption_profile(
268        &self,
269        route: &Route,
270    ) -> Result<AwsResponse, AwsServiceError> {
271        let id = route_id(route, "FieldLevelEncryptionProfile")?;
272        let state = self.state.read();
273        let p = state
274            .accounts
275            .get(DEFAULT_ACCOUNT)
276            .and_then(|a| a.field_level_encryption_profiles.get(&id).cloned())
277            .ok_or_else(|| not_found("FieldLevelEncryptionProfile", &id))?;
278        drop(state);
279        let body = render_fle_profile(&p);
280        Ok(xml_with_etag(StatusCode::OK, body, &p.etag, None))
281    }
282
283    pub(crate) fn get_field_level_encryption_profile_config(
284        &self,
285        route: &Route,
286    ) -> Result<AwsResponse, AwsServiceError> {
287        let id = route_id(route, "FieldLevelEncryptionProfile")?;
288        let state = self.state.read();
289        let p = state
290            .accounts
291            .get(DEFAULT_ACCOUNT)
292            .and_then(|a| a.field_level_encryption_profiles.get(&id).cloned())
293            .ok_or_else(|| not_found("FieldLevelEncryptionProfile", &id))?;
294        drop(state);
295        let body = render_fle_profile_config(&p.config);
296        Ok(xml_with_etag(StatusCode::OK, body, &p.etag, None))
297    }
298
299    pub(crate) fn update_field_level_encryption_profile(
300        &self,
301        req: &AwsRequest,
302        route: &Route,
303    ) -> Result<AwsResponse, AwsServiceError> {
304        let id = route_id(route, "FieldLevelEncryptionProfile")?;
305        let if_match = require_if_match(req)?;
306        let cfg: FieldLevelEncryptionProfileConfig =
307            xml_io::from_xml_root(&req.body).map_err(|e| {
308                invalid_argument(format!(
309                    "invalid FieldLevelEncryptionProfileConfig XML: {e}"
310                ))
311            })?;
312        if cfg.name.is_empty() {
313            return Err(invalid_argument(
314                "FieldLevelEncryptionProfileConfig.Name is required",
315            ));
316        }
317        let mut state = self.state.write();
318        let account = state
319            .accounts
320            .get_mut(DEFAULT_ACCOUNT)
321            .ok_or_else(|| not_found("FieldLevelEncryptionProfile", &id))?;
322        let p = account
323            .field_level_encryption_profiles
324            .get_mut(&id)
325            .ok_or_else(|| not_found("FieldLevelEncryptionProfile", &id))?;
326        if p.etag != if_match {
327            return Err(precondition_failed());
328        }
329        if p.config.caller_reference != cfg.caller_reference {
330            return Err(invalid_argument(
331                "CallerReference cannot change on UpdateFieldLevelEncryptionProfile",
332            ));
333        }
334        p.config = cfg;
335        p.etag = generate_id_with_prefix("E");
336        p.last_modified_time = Utc::now();
337        let snap = p.clone();
338        drop(state);
339        let body = render_fle_profile(&snap);
340        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
341    }
342
343    pub(crate) fn delete_field_level_encryption_profile(
344        &self,
345        req: &AwsRequest,
346        route: &Route,
347    ) -> Result<AwsResponse, AwsServiceError> {
348        let id = route_id(route, "FieldLevelEncryptionProfile")?;
349        let if_match = require_if_match(req)?;
350        let mut state = self.state.write();
351        let account = state
352            .accounts
353            .get_mut(DEFAULT_ACCOUNT)
354            .ok_or_else(|| not_found("FieldLevelEncryptionProfile", &id))?;
355        let p = account
356            .field_level_encryption_profiles
357            .get(&id)
358            .ok_or_else(|| not_found("FieldLevelEncryptionProfile", &id))?;
359        if p.etag != if_match {
360            return Err(precondition_failed());
361        }
362        account.field_level_encryption_profiles.remove(&id);
363        drop(state);
364        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
365    }
366
367    pub(crate) fn list_field_level_encryption_profiles(
368        &self,
369        _req: &AwsRequest,
370    ) -> Result<AwsResponse, AwsServiceError> {
371        let state = self.state.read();
372        let mut items: Vec<StoredFieldLevelEncryptionProfile> = state
373            .accounts
374            .get(DEFAULT_ACCOUNT)
375            .map(|a| {
376                a.field_level_encryption_profiles
377                    .values()
378                    .cloned()
379                    .collect()
380            })
381            .unwrap_or_default();
382        drop(state);
383        items.sort_by(|a, b| a.config.name.cmp(&b.config.name));
384
385        let mut body = String::with_capacity(512);
386        body.push_str(XML_DECL);
387        body.push_str(&format!("<FieldLevelEncryptionProfileList xmlns=\"{NS}\">"));
388        body.push_str("<NextMarker></NextMarker>");
389        body.push_str("<MaxItems>100</MaxItems>");
390        body.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
391        body.push_str("<Items>");
392        for p in &items {
393            body.push_str("<FieldLevelEncryptionProfileSummary>");
394            body.push_str(&format!("<Id>{}</Id>", esc(&p.id)));
395            body.push_str(&format!(
396                "<LastModifiedTime>{}</LastModifiedTime>",
397                rfc3339(&p.last_modified_time)
398            ));
399            body.push_str(&format!("<Name>{}</Name>", esc(&p.config.name)));
400            body.push_str(&render_encryption_entities(&p.config.encryption_entities));
401            if let Some(c) = &p.config.comment {
402                body.push_str(&format!("<Comment>{}</Comment>", esc(c)));
403            }
404            body.push_str("</FieldLevelEncryptionProfileSummary>");
405        }
406        body.push_str("</Items>");
407        body.push_str("</FieldLevelEncryptionProfileList>");
408        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
409    }
410}
411
412// ─── Realtime Log Config ──────────────────────────────────────────────
413
414impl CloudFrontService {
415    pub(crate) fn create_realtime_log_config(
416        &self,
417        req: &AwsRequest,
418    ) -> Result<AwsResponse, AwsServiceError> {
419        let parsed: CreateRealtimeLogConfigRequest =
420            xml_io::from_xml_root(&req.body).map_err(|e| {
421                invalid_argument(format!("invalid CreateRealtimeLogConfigRequest XML: {e}"))
422            })?;
423        if parsed.name.is_empty() {
424            return Err(invalid_argument("Name is required"));
425        }
426        let mut state = self.state.write();
427        let account = state
428            .accounts
429            .entry(DEFAULT_ACCOUNT.to_string())
430            .or_default();
431        let arn = format!(
432            "arn:aws:cloudfront::{}:realtime-log-config/{}",
433            DEFAULT_ACCOUNT, parsed.name
434        );
435        if account.realtime_log_configs.contains_key(&arn) {
436            return Err(aws_error(
437                StatusCode::CONFLICT,
438                "RealtimeLogConfigAlreadyExists",
439                format!("RealtimeLogConfig {} already exists", parsed.name),
440            ));
441        }
442        let stored = StoredRealtimeLogConfig {
443            arn: arn.clone(),
444            name: parsed.name,
445            sampling_rate: parsed.sampling_rate,
446            end_points: parsed.end_points,
447            fields: parsed.fields,
448        };
449        account
450            .realtime_log_configs
451            .insert(arn.clone(), stored.clone());
452        drop(state);
453        let body = render_realtime_log(&stored, "CreateRealtimeLogConfigResult");
454        Ok(xml_response(StatusCode::CREATED, body, HeaderMap::new()))
455    }
456
457    pub(crate) fn get_realtime_log_config(
458        &self,
459        req: &AwsRequest,
460    ) -> Result<AwsResponse, AwsServiceError> {
461        let parsed: GetOrDeleteRealtimeLogConfigRequest = xml_io::from_xml_root(&req.body)
462            .map_err(|e| {
463                invalid_argument(format!("invalid GetRealtimeLogConfigRequest XML: {e}"))
464            })?;
465        let key = self.resolve_rtl_key(&parsed)?;
466        let state = self.state.read();
467        let r = state
468            .accounts
469            .get(DEFAULT_ACCOUNT)
470            .and_then(|a| a.realtime_log_configs.get(&key).cloned())
471            .ok_or_else(|| not_found("RealtimeLogConfig", &key))?;
472        drop(state);
473        let body = render_realtime_log(&r, "GetRealtimeLogConfigResult");
474        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
475    }
476
477    pub(crate) fn update_realtime_log_config(
478        &self,
479        req: &AwsRequest,
480    ) -> Result<AwsResponse, AwsServiceError> {
481        let parsed: UpdateRealtimeLogConfigRequest =
482            xml_io::from_xml_root(&req.body).map_err(|e| {
483                invalid_argument(format!("invalid UpdateRealtimeLogConfigRequest XML: {e}"))
484            })?;
485        if parsed.arn.is_empty() {
486            return Err(invalid_argument("ARN is required"));
487        }
488        let mut state = self.state.write();
489        let account = state
490            .accounts
491            .get_mut(DEFAULT_ACCOUNT)
492            .ok_or_else(|| not_found("RealtimeLogConfig", &parsed.arn))?;
493        let r = account
494            .realtime_log_configs
495            .get_mut(&parsed.arn)
496            .ok_or_else(|| not_found("RealtimeLogConfig", &parsed.arn))?;
497        r.sampling_rate = parsed.sampling_rate;
498        r.end_points = parsed.end_points;
499        r.fields = parsed.fields;
500        let snap = r.clone();
501        drop(state);
502        let body = render_realtime_log(&snap, "UpdateRealtimeLogConfigResult");
503        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
504    }
505
506    pub(crate) fn delete_realtime_log_config(
507        &self,
508        req: &AwsRequest,
509    ) -> Result<AwsResponse, AwsServiceError> {
510        let parsed: GetOrDeleteRealtimeLogConfigRequest = xml_io::from_xml_root(&req.body)
511            .map_err(|e| {
512                invalid_argument(format!("invalid DeleteRealtimeLogConfigRequest XML: {e}"))
513            })?;
514        let key = self.resolve_rtl_key(&parsed)?;
515        let mut state = self.state.write();
516        let account = state
517            .accounts
518            .get_mut(DEFAULT_ACCOUNT)
519            .ok_or_else(|| not_found("RealtimeLogConfig", &key))?;
520        if account.realtime_log_configs.remove(&key).is_none() {
521            return Err(not_found("RealtimeLogConfig", &key));
522        }
523        drop(state);
524        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
525    }
526
527    pub(crate) fn list_realtime_log_configs(
528        &self,
529        _req: &AwsRequest,
530    ) -> Result<AwsResponse, AwsServiceError> {
531        let state = self.state.read();
532        let mut items: Vec<StoredRealtimeLogConfig> = state
533            .accounts
534            .get(DEFAULT_ACCOUNT)
535            .map(|a| a.realtime_log_configs.values().cloned().collect())
536            .unwrap_or_default();
537        drop(state);
538        items.sort_by(|a, b| a.name.cmp(&b.name));
539
540        let mut body = String::with_capacity(512);
541        body.push_str(XML_DECL);
542        body.push_str(&format!("<RealtimeLogConfigs xmlns=\"{NS}\">"));
543        body.push_str("<MaxItems>100</MaxItems>");
544        body.push_str(&format!("<IsTruncated>{}</IsTruncated>", false));
545        body.push_str("<Marker></Marker>");
546        body.push_str("<Items>");
547        for r in &items {
548            body.push_str("<member>");
549            push_realtime_log_inner(&mut body, r);
550            body.push_str("</member>");
551        }
552        body.push_str("</Items>");
553        body.push_str("</RealtimeLogConfigs>");
554        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
555    }
556
557    fn resolve_rtl_key(
558        &self,
559        parsed: &GetOrDeleteRealtimeLogConfigRequest,
560    ) -> Result<String, AwsServiceError> {
561        if let Some(arn) = &parsed.arn {
562            if !arn.is_empty() {
563                return Ok(arn.clone());
564            }
565        }
566        if let Some(name) = &parsed.name {
567            if !name.is_empty() {
568                return Ok(format!(
569                    "arn:aws:cloudfront::{}:realtime-log-config/{}",
570                    DEFAULT_ACCOUNT, name
571                ));
572            }
573        }
574        Err(invalid_argument("Either Name or ARN must be specified"))
575    }
576}
577
578// ─── XML render helpers ───────────────────────────────────────────────
579
580fn render_fle(f: &StoredFieldLevelEncryption) -> String {
581    let mut out = String::with_capacity(512);
582    out.push_str(XML_DECL);
583    out.push_str(&format!("<FieldLevelEncryption xmlns=\"{NS}\">"));
584    out.push_str(&format!("<Id>{}</Id>", esc(&f.id)));
585    out.push_str(&format!(
586        "<LastModifiedTime>{}</LastModifiedTime>",
587        rfc3339(&f.last_modified_time)
588    ));
589    out.push_str(&render_fle_config_inner(&f.config));
590    out.push_str("</FieldLevelEncryption>");
591    out
592}
593
594fn render_fle_config(cfg: &FieldLevelEncryptionConfig) -> String {
595    let mut out = String::with_capacity(512);
596    out.push_str(XML_DECL);
597    out.push_str(&format!("<FieldLevelEncryptionConfig xmlns=\"{NS}\">"));
598    out.push_str(&render_fle_config_body(cfg));
599    out.push_str("</FieldLevelEncryptionConfig>");
600    out
601}
602
603fn render_fle_config_inner(cfg: &FieldLevelEncryptionConfig) -> String {
604    let mut out = String::with_capacity(512);
605    out.push_str("<FieldLevelEncryptionConfig>");
606    out.push_str(&render_fle_config_body(cfg));
607    out.push_str("</FieldLevelEncryptionConfig>");
608    out
609}
610
611fn render_fle_config_body(cfg: &FieldLevelEncryptionConfig) -> String {
612    let mut out = String::with_capacity(512);
613    out.push_str(&format!(
614        "<CallerReference>{}</CallerReference>",
615        esc(&cfg.caller_reference)
616    ));
617    if let Some(c) = &cfg.comment {
618        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
619    }
620    out.push_str(&render_query_arg_profile_config(
621        &cfg.query_arg_profile_config,
622    ));
623    out.push_str(&render_content_type_profile_config(
624        &cfg.content_type_profile_config,
625    ));
626    out
627}
628
629fn render_query_arg_profile_config(cfg: &crate::fle::QueryArgProfileConfig) -> String {
630    let mut out = String::with_capacity(128);
631    out.push_str("<QueryArgProfileConfig>");
632    out.push_str(&format!(
633        "<ForwardWhenQueryArgProfileIsUnknown>{}</ForwardWhenQueryArgProfileIsUnknown>",
634        cfg.forward_when_query_arg_profile_is_unknown
635    ));
636    if let Some(qp) = &cfg.query_arg_profiles {
637        out.push_str("<QueryArgProfiles>");
638        out.push_str(&format!("<Quantity>{}</Quantity>", qp.quantity));
639        if let Some(items) = &qp.items {
640            out.push_str("<Items>");
641            for q in &items.query_arg_profile {
642                out.push_str("<QueryArgProfile>");
643                out.push_str(&format!("<QueryArg>{}</QueryArg>", esc(&q.query_arg)));
644                out.push_str(&format!("<ProfileId>{}</ProfileId>", esc(&q.profile_id)));
645                out.push_str("</QueryArgProfile>");
646            }
647            out.push_str("</Items>");
648        }
649        out.push_str("</QueryArgProfiles>");
650    }
651    out.push_str("</QueryArgProfileConfig>");
652    out
653}
654
655fn render_content_type_profile_config(cfg: &crate::fle::ContentTypeProfileConfig) -> String {
656    let mut out = String::with_capacity(128);
657    out.push_str("<ContentTypeProfileConfig>");
658    out.push_str(&format!(
659        "<ForwardWhenContentTypeIsUnknown>{}</ForwardWhenContentTypeIsUnknown>",
660        cfg.forward_when_content_type_is_unknown
661    ));
662    if let Some(ct) = &cfg.content_type_profiles {
663        out.push_str("<ContentTypeProfiles>");
664        out.push_str(&format!("<Quantity>{}</Quantity>", ct.quantity));
665        if let Some(items) = &ct.items {
666            out.push_str("<Items>");
667            for c in &items.content_type_profile {
668                out.push_str("<ContentTypeProfile>");
669                out.push_str(&format!("<Format>{}</Format>", esc(&c.format)));
670                if let Some(p) = &c.profile_id {
671                    out.push_str(&format!("<ProfileId>{}</ProfileId>", esc(p)));
672                }
673                out.push_str(&format!(
674                    "<ContentType>{}</ContentType>",
675                    esc(&c.content_type)
676                ));
677                out.push_str("</ContentTypeProfile>");
678            }
679            out.push_str("</Items>");
680        }
681        out.push_str("</ContentTypeProfiles>");
682    }
683    out.push_str("</ContentTypeProfileConfig>");
684    out
685}
686
687fn render_fle_profile(p: &StoredFieldLevelEncryptionProfile) -> String {
688    let mut out = String::with_capacity(512);
689    out.push_str(XML_DECL);
690    out.push_str(&format!("<FieldLevelEncryptionProfile xmlns=\"{NS}\">"));
691    out.push_str(&format!("<Id>{}</Id>", esc(&p.id)));
692    out.push_str(&format!(
693        "<LastModifiedTime>{}</LastModifiedTime>",
694        rfc3339(&p.last_modified_time)
695    ));
696    out.push_str(&render_fle_profile_config_inner(&p.config));
697    out.push_str("</FieldLevelEncryptionProfile>");
698    out
699}
700
701fn render_fle_profile_config(cfg: &FieldLevelEncryptionProfileConfig) -> String {
702    let mut out = String::with_capacity(512);
703    out.push_str(XML_DECL);
704    out.push_str(&format!(
705        "<FieldLevelEncryptionProfileConfig xmlns=\"{NS}\">"
706    ));
707    out.push_str(&render_fle_profile_config_body(cfg));
708    out.push_str("</FieldLevelEncryptionProfileConfig>");
709    out
710}
711
712fn render_fle_profile_config_inner(cfg: &FieldLevelEncryptionProfileConfig) -> String {
713    let mut out = String::with_capacity(512);
714    out.push_str("<FieldLevelEncryptionProfileConfig>");
715    out.push_str(&render_fle_profile_config_body(cfg));
716    out.push_str("</FieldLevelEncryptionProfileConfig>");
717    out
718}
719
720fn render_fle_profile_config_body(cfg: &FieldLevelEncryptionProfileConfig) -> String {
721    let mut out = String::with_capacity(512);
722    out.push_str(&format!("<Name>{}</Name>", esc(&cfg.name)));
723    out.push_str(&format!(
724        "<CallerReference>{}</CallerReference>",
725        esc(&cfg.caller_reference)
726    ));
727    if let Some(c) = &cfg.comment {
728        out.push_str(&format!("<Comment>{}</Comment>", esc(c)));
729    }
730    out.push_str(&render_encryption_entities(&cfg.encryption_entities));
731    out
732}
733
734fn render_encryption_entities(ee: &crate::fle::EncryptionEntities) -> String {
735    let mut out = String::with_capacity(128);
736    out.push_str("<EncryptionEntities>");
737    out.push_str(&format!("<Quantity>{}</Quantity>", ee.quantity));
738    if let Some(items) = &ee.items {
739        out.push_str("<Items>");
740        for e in &items.encryption_entity {
741            out.push_str("<EncryptionEntity>");
742            out.push_str(&format!(
743                "<PublicKeyId>{}</PublicKeyId>",
744                esc(&e.public_key_id)
745            ));
746            out.push_str(&format!("<ProviderId>{}</ProviderId>", esc(&e.provider_id)));
747            out.push_str("<FieldPatterns>");
748            out.push_str(&format!(
749                "<Quantity>{}</Quantity>",
750                e.field_patterns.quantity
751            ));
752            if let Some(it) = &e.field_patterns.items {
753                out.push_str("<Items>");
754                for fp in &it.field_pattern {
755                    out.push_str(&format!("<FieldPattern>{}</FieldPattern>", esc(fp)));
756                }
757                out.push_str("</Items>");
758            }
759            out.push_str("</FieldPatterns>");
760            out.push_str("</EncryptionEntity>");
761        }
762        out.push_str("</Items>");
763    }
764    out.push_str("</EncryptionEntities>");
765    out
766}
767
768fn render_realtime_log(r: &StoredRealtimeLogConfig, root: &str) -> String {
769    let mut out = String::with_capacity(512);
770    out.push_str(XML_DECL);
771    out.push_str(&format!("<{root} xmlns=\"{NS}\">"));
772    out.push_str("<RealtimeLogConfig>");
773    push_realtime_log_inner(&mut out, r);
774    out.push_str("</RealtimeLogConfig>");
775    out.push_str(&format!("</{root}>"));
776    out
777}
778
779fn push_realtime_log_inner(out: &mut String, r: &StoredRealtimeLogConfig) {
780    out.push_str(&format!("<ARN>{}</ARN>", esc(&r.arn)));
781    out.push_str(&format!("<Name>{}</Name>", esc(&r.name)));
782    out.push_str(&format!("<SamplingRate>{}</SamplingRate>", r.sampling_rate));
783    out.push_str("<EndPoints>");
784    for ep in &r.end_points.member {
785        out.push_str("<member>");
786        out.push_str(&format!(
787            "<StreamType>{}</StreamType>",
788            esc(&ep.stream_type)
789        ));
790        out.push_str("<KinesisStreamConfig>");
791        out.push_str(&format!(
792            "<RoleARN>{}</RoleARN>",
793            esc(&ep.kinesis_stream_config.role_arn)
794        ));
795        out.push_str(&format!(
796            "<StreamARN>{}</StreamARN>",
797            esc(&ep.kinesis_stream_config.stream_arn)
798        ));
799        out.push_str("</KinesisStreamConfig>");
800        out.push_str("</member>");
801    }
802    out.push_str("</EndPoints>");
803    out.push_str("<Fields>");
804    for f in &r.fields.field {
805        out.push_str(&format!("<Field>{}</Field>", esc(f)));
806    }
807    out.push_str("</Fields>");
808}