Skip to main content

winterbaume_backup/
views.rs

1//! Serde-compatible view types for Backup state snapshots.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use winterbaume_core::{StateChangeNotifier, StateViewError, StatefulService};
7
8use crate::handlers::BackupService;
9use crate::state::BackupState;
10use crate::types::{
11    BackupPlanData, BackupVault, FrameworkData, LegalHoldData, ReportPlanData,
12    RestoreTestingPlanData, RestoreTestingSelectionData, TieringConfigData, VaultAccessPolicy,
13    VaultNotificationConfig,
14};
15
16/// Serializable view of the entire Backup state for one account/region.
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct BackupStateView {
19    /// Backup vaults keyed by vault name.
20    #[serde(default)]
21    pub vaults: HashMap<String, BackupVaultView>,
22    /// Backup plans keyed by plan ID.
23    #[serde(default)]
24    pub backup_plans: HashMap<String, BackupPlanView>,
25    /// Report plans keyed by report plan name.
26    #[serde(default)]
27    pub report_plans: HashMap<String, ReportPlanView>,
28    /// Tags keyed by resource ARN.
29    #[serde(default)]
30    pub resource_tags: HashMap<String, HashMap<String, String>>,
31    /// Audit frameworks keyed by framework name.
32    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
33    pub frameworks: HashMap<String, FrameworkView>,
34    /// Vault access policies keyed by vault name.
35    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
36    pub vault_access_policies: HashMap<String, VaultAccessPolicyView>,
37    /// Vault notification configurations keyed by vault name.
38    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
39    pub vault_notifications: HashMap<String, VaultNotificationConfigView>,
40    /// Tiering configurations keyed by configuration name.
41    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
42    pub tiering_configs: HashMap<String, TieringConfigView>,
43    /// Legal holds keyed by legal_hold_id.
44    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
45    pub legal_holds: HashMap<String, LegalHoldView>,
46    /// Restore testing plans keyed by plan name.
47    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
48    pub restore_testing_plans: HashMap<String, RestoreTestingPlanView>,
49    /// Restore testing selections keyed by "plan_name/selection_name".
50    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
51    pub restore_testing_selections: HashMap<String, RestoreTestingSelectionView>,
52}
53
54/// Serializable view of a backup vault.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct BackupVaultView {
57    pub backup_vault_name: String,
58    pub backup_vault_arn: String,
59    pub creation_date: String,
60    pub number_of_recovery_points: i64,
61    pub locked: bool,
62    pub min_retention_days: Option<i64>,
63    pub max_retention_days: Option<i64>,
64    pub lock_date: Option<String>,
65    #[serde(default)]
66    pub tags: HashMap<String, String>,
67}
68
69/// Serializable view of a backup plan.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BackupPlanView {
72    pub backup_plan_id: String,
73    pub backup_plan_arn: String,
74    pub backup_plan_name: String,
75    pub version_id: String,
76    pub creation_date: String,
77    pub backup_plan_json: serde_json::Value,
78    #[serde(default)]
79    pub tags: HashMap<String, String>,
80}
81
82/// Serializable view of a report plan.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ReportPlanView {
85    pub report_plan_name: String,
86    pub report_plan_arn: String,
87    pub report_plan_description: String,
88    pub report_delivery_channel: serde_json::Value,
89    pub report_setting: serde_json::Value,
90    pub creation_time: String,
91    pub deployment_status: String,
92    #[serde(default)]
93    pub tags: HashMap<String, String>,
94}
95
96/// Serializable view of an audit framework.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct FrameworkView {
99    pub framework_name: String,
100    pub framework_arn: String,
101    pub framework_description: String,
102    pub framework_controls: serde_json::Value,
103    pub creation_time: String,
104    pub deployment_status: String,
105    pub number_of_controls: i32,
106}
107
108/// Serializable view of a vault access policy.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct VaultAccessPolicyView {
111    pub backup_vault_name: String,
112    pub backup_vault_arn: String,
113    pub policy: String,
114}
115
116/// Serializable view of a vault notification configuration.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct VaultNotificationConfigView {
119    pub backup_vault_name: String,
120    pub backup_vault_arn: String,
121    pub sns_topic_arn: String,
122    #[serde(default)]
123    pub backup_vault_events: Vec<String>,
124}
125
126/// Serializable view of a tiering configuration.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct TieringConfigView {
129    pub tiering_configuration_name: String,
130    pub tiering_configuration_arn: String,
131    pub backup_vault_name: String,
132    pub resource_selection: serde_json::Value,
133    pub creation_time: String,
134    pub last_updated_time: String,
135    #[serde(default)]
136    pub creator_request_id: Option<String>,
137    #[serde(default)]
138    pub tags: HashMap<String, String>,
139}
140
141/// Serializable view of a legal hold.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct LegalHoldView {
144    pub legal_hold_id: String,
145    pub legal_hold_arn: String,
146    pub title: String,
147    pub description: String,
148    pub status: String,
149    pub creation_date: String,
150    #[serde(default)]
151    pub cancellation_date: Option<String>,
152    pub recovery_point_selection: serde_json::Value,
153    #[serde(default)]
154    pub tags: HashMap<String, String>,
155}
156
157/// Serializable view of a restore testing plan.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct RestoreTestingPlanView {
160    pub restore_testing_plan_name: String,
161    pub restore_testing_plan_arn: String,
162    pub schedule_expression: String,
163    #[serde(default)]
164    pub schedule_expression_timezone: Option<String>,
165    #[serde(default)]
166    pub start_window_hours: Option<i32>,
167    pub recovery_point_selection: serde_json::Value,
168    #[serde(default)]
169    pub creator_request_id: Option<String>,
170    pub creation_time: String,
171    pub last_update_time: String,
172    #[serde(default)]
173    pub tags: HashMap<String, String>,
174}
175
176/// Serializable view of a restore testing selection.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct RestoreTestingSelectionView {
179    pub restore_testing_selection_name: String,
180    pub restore_testing_plan_name: String,
181    pub restore_testing_plan_arn: String,
182    pub iam_role_arn: String,
183    pub protected_resource_type: String,
184    #[serde(default)]
185    pub protected_resource_arns: Vec<String>,
186    pub protected_resource_conditions: serde_json::Value,
187    #[serde(default)]
188    pub restore_metadata_overrides: HashMap<String, String>,
189    #[serde(default)]
190    pub validation_window_hours: Option<i32>,
191    #[serde(default)]
192    pub creator_request_id: Option<String>,
193    pub creation_time: String,
194    pub last_update_time: String,
195}
196
197// ---------------------------------------------------------------------------
198// From conversions
199// ---------------------------------------------------------------------------
200
201impl From<&BackupVault> for BackupVaultView {
202    fn from(v: &BackupVault) -> Self {
203        BackupVaultView {
204            backup_vault_name: v.backup_vault_name.clone(),
205            backup_vault_arn: v.backup_vault_arn.clone(),
206            creation_date: v.creation_date.to_rfc3339(),
207            number_of_recovery_points: v.number_of_recovery_points,
208            locked: v.locked,
209            min_retention_days: v.min_retention_days,
210            max_retention_days: v.max_retention_days,
211            lock_date: v.lock_date.as_ref().map(|d| d.to_rfc3339()),
212            tags: v.tags.clone(),
213        }
214    }
215}
216
217impl From<&BackupPlanData> for BackupPlanView {
218    fn from(p: &BackupPlanData) -> Self {
219        BackupPlanView {
220            backup_plan_id: p.backup_plan_id.clone(),
221            backup_plan_arn: p.backup_plan_arn.clone(),
222            backup_plan_name: p.backup_plan_name.clone(),
223            version_id: p.version_id.clone(),
224            creation_date: p.creation_date.to_rfc3339(),
225            backup_plan_json: p.backup_plan_json.clone(),
226            tags: p.tags.clone(),
227        }
228    }
229}
230
231impl From<&ReportPlanData> for ReportPlanView {
232    fn from(r: &ReportPlanData) -> Self {
233        ReportPlanView {
234            report_plan_name: r.report_plan_name.clone(),
235            report_plan_arn: r.report_plan_arn.clone(),
236            report_plan_description: r.report_plan_description.clone(),
237            report_delivery_channel: r.report_delivery_channel.clone(),
238            report_setting: r.report_setting.clone(),
239            creation_time: r.creation_time.to_rfc3339(),
240            deployment_status: r.deployment_status.clone(),
241            tags: r.tags.clone(),
242        }
243    }
244}
245
246impl From<&FrameworkData> for FrameworkView {
247    fn from(f: &FrameworkData) -> Self {
248        FrameworkView {
249            framework_name: f.framework_name.clone(),
250            framework_arn: f.framework_arn.clone(),
251            framework_description: f.framework_description.clone(),
252            framework_controls: f.framework_controls.clone(),
253            creation_time: f.creation_time.to_rfc3339(),
254            deployment_status: f.deployment_status.clone(),
255            number_of_controls: f.number_of_controls,
256        }
257    }
258}
259
260impl From<&VaultAccessPolicy> for VaultAccessPolicyView {
261    fn from(p: &VaultAccessPolicy) -> Self {
262        VaultAccessPolicyView {
263            backup_vault_name: p.backup_vault_name.clone(),
264            backup_vault_arn: p.backup_vault_arn.clone(),
265            policy: p.policy.clone(),
266        }
267    }
268}
269
270impl From<&VaultNotificationConfig> for VaultNotificationConfigView {
271    fn from(n: &VaultNotificationConfig) -> Self {
272        VaultNotificationConfigView {
273            backup_vault_name: n.backup_vault_name.clone(),
274            backup_vault_arn: n.backup_vault_arn.clone(),
275            sns_topic_arn: n.sns_topic_arn.clone(),
276            backup_vault_events: n.backup_vault_events.clone(),
277        }
278    }
279}
280
281impl From<&TieringConfigData> for TieringConfigView {
282    fn from(t: &TieringConfigData) -> Self {
283        TieringConfigView {
284            tiering_configuration_name: t.tiering_configuration_name.clone(),
285            tiering_configuration_arn: t.tiering_configuration_arn.clone(),
286            backup_vault_name: t.backup_vault_name.clone(),
287            resource_selection: t.resource_selection.clone(),
288            creation_time: t.creation_time.to_rfc3339(),
289            last_updated_time: t.last_updated_time.to_rfc3339(),
290            creator_request_id: t.creator_request_id.clone(),
291            tags: t.tags.clone(),
292        }
293    }
294}
295
296impl From<&LegalHoldData> for LegalHoldView {
297    fn from(h: &LegalHoldData) -> Self {
298        LegalHoldView {
299            legal_hold_id: h.legal_hold_id.clone(),
300            legal_hold_arn: h.legal_hold_arn.clone(),
301            title: h.title.clone(),
302            description: h.description.clone(),
303            status: h.status.clone(),
304            creation_date: h.creation_date.to_rfc3339(),
305            cancellation_date: h.cancellation_date.as_ref().map(|d| d.to_rfc3339()),
306            recovery_point_selection: h.recovery_point_selection.clone(),
307            tags: h.tags.clone(),
308        }
309    }
310}
311
312impl From<&RestoreTestingPlanData> for RestoreTestingPlanView {
313    fn from(p: &RestoreTestingPlanData) -> Self {
314        RestoreTestingPlanView {
315            restore_testing_plan_name: p.restore_testing_plan_name.clone(),
316            restore_testing_plan_arn: p.restore_testing_plan_arn.clone(),
317            schedule_expression: p.schedule_expression.clone(),
318            schedule_expression_timezone: p.schedule_expression_timezone.clone(),
319            start_window_hours: p.start_window_hours,
320            recovery_point_selection: p.recovery_point_selection.clone(),
321            creator_request_id: p.creator_request_id.clone(),
322            creation_time: p.creation_time.to_rfc3339(),
323            last_update_time: p.last_update_time.to_rfc3339(),
324            tags: p.tags.clone(),
325        }
326    }
327}
328
329impl From<&RestoreTestingSelectionData> for RestoreTestingSelectionView {
330    fn from(s: &RestoreTestingSelectionData) -> Self {
331        RestoreTestingSelectionView {
332            restore_testing_selection_name: s.restore_testing_selection_name.clone(),
333            restore_testing_plan_name: s.restore_testing_plan_name.clone(),
334            restore_testing_plan_arn: s.restore_testing_plan_arn.clone(),
335            iam_role_arn: s.iam_role_arn.clone(),
336            protected_resource_type: s.protected_resource_type.clone(),
337            protected_resource_arns: s.protected_resource_arns.clone(),
338            protected_resource_conditions: s.protected_resource_conditions.clone(),
339            restore_metadata_overrides: s.restore_metadata_overrides.clone(),
340            validation_window_hours: s.validation_window_hours,
341            creator_request_id: s.creator_request_id.clone(),
342            creation_time: s.creation_time.to_rfc3339(),
343            last_update_time: s.last_update_time.to_rfc3339(),
344        }
345    }
346}
347
348impl From<&BackupState> for BackupStateView {
349    fn from(s: &BackupState) -> Self {
350        let vaults = s
351            .vaults
352            .iter()
353            .map(|(k, v)| (k.clone(), BackupVaultView::from(v)))
354            .collect();
355        let backup_plans = s
356            .backup_plans
357            .iter()
358            .map(|(k, v)| (k.clone(), BackupPlanView::from(v)))
359            .collect();
360        let report_plans = s
361            .report_plans
362            .iter()
363            .map(|(k, v)| (k.clone(), ReportPlanView::from(v)))
364            .collect();
365        let frameworks = s
366            .frameworks
367            .iter()
368            .map(|(k, v)| (k.clone(), FrameworkView::from(v)))
369            .collect();
370        let vault_access_policies = s
371            .vault_access_policies
372            .iter()
373            .map(|(k, v)| (k.clone(), VaultAccessPolicyView::from(v)))
374            .collect();
375        let vault_notifications = s
376            .vault_notifications
377            .iter()
378            .map(|(k, v)| (k.clone(), VaultNotificationConfigView::from(v)))
379            .collect();
380        let tiering_configs = s
381            .tiering_configs
382            .iter()
383            .map(|(k, v)| (k.clone(), TieringConfigView::from(v)))
384            .collect();
385        let legal_holds = s
386            .legal_holds
387            .iter()
388            .map(|(k, v)| (k.clone(), LegalHoldView::from(v)))
389            .collect();
390        let restore_testing_plans = s
391            .restore_testing_plans
392            .iter()
393            .map(|(k, v)| (k.clone(), RestoreTestingPlanView::from(v)))
394            .collect();
395        let restore_testing_selections = s
396            .restore_testing_selections
397            .iter()
398            .map(|((plan, sel), v)| {
399                (
400                    format!("{plan}/{sel}"),
401                    RestoreTestingSelectionView::from(v),
402                )
403            })
404            .collect();
405        BackupStateView {
406            vaults,
407            backup_plans,
408            report_plans,
409            resource_tags: s.resource_tags.clone(),
410            frameworks,
411            vault_access_policies,
412            vault_notifications,
413            tiering_configs,
414            legal_holds,
415            restore_testing_plans,
416            restore_testing_selections,
417        }
418    }
419}
420
421fn parse_rfc3339_or_now(s: &str) -> chrono::DateTime<chrono::Utc> {
422    use chrono::{DateTime, Utc};
423    DateTime::parse_from_rfc3339(s)
424        .map(|d| d.with_timezone(&Utc))
425        .unwrap_or_else(|_| Utc::now())
426}
427
428fn parse_rfc3339_opt(s: Option<String>) -> Option<chrono::DateTime<chrono::Utc>> {
429    use chrono::{DateTime, Utc};
430    s.as_deref()
431        .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
432        .map(|d| d.with_timezone(&Utc))
433}
434
435impl From<FrameworkView> for FrameworkData {
436    fn from(v: FrameworkView) -> Self {
437        FrameworkData {
438            framework_name: v.framework_name,
439            framework_arn: v.framework_arn,
440            framework_description: v.framework_description,
441            framework_controls: v.framework_controls,
442            creation_time: parse_rfc3339_or_now(&v.creation_time),
443            deployment_status: v.deployment_status,
444            number_of_controls: v.number_of_controls,
445        }
446    }
447}
448
449impl From<VaultAccessPolicyView> for VaultAccessPolicy {
450    fn from(v: VaultAccessPolicyView) -> Self {
451        VaultAccessPolicy {
452            backup_vault_name: v.backup_vault_name,
453            backup_vault_arn: v.backup_vault_arn,
454            policy: v.policy,
455        }
456    }
457}
458
459impl From<VaultNotificationConfigView> for VaultNotificationConfig {
460    fn from(v: VaultNotificationConfigView) -> Self {
461        VaultNotificationConfig {
462            backup_vault_name: v.backup_vault_name,
463            backup_vault_arn: v.backup_vault_arn,
464            sns_topic_arn: v.sns_topic_arn,
465            backup_vault_events: v.backup_vault_events,
466        }
467    }
468}
469
470impl From<TieringConfigView> for TieringConfigData {
471    fn from(v: TieringConfigView) -> Self {
472        TieringConfigData {
473            tiering_configuration_name: v.tiering_configuration_name,
474            tiering_configuration_arn: v.tiering_configuration_arn,
475            backup_vault_name: v.backup_vault_name,
476            resource_selection: v.resource_selection,
477            creation_time: parse_rfc3339_or_now(&v.creation_time),
478            last_updated_time: parse_rfc3339_or_now(&v.last_updated_time),
479            creator_request_id: v.creator_request_id,
480            tags: v.tags,
481        }
482    }
483}
484
485impl From<LegalHoldView> for LegalHoldData {
486    fn from(v: LegalHoldView) -> Self {
487        LegalHoldData {
488            legal_hold_id: v.legal_hold_id,
489            legal_hold_arn: v.legal_hold_arn,
490            title: v.title,
491            description: v.description,
492            status: v.status,
493            creation_date: parse_rfc3339_or_now(&v.creation_date),
494            cancellation_date: parse_rfc3339_opt(v.cancellation_date),
495            recovery_point_selection: v.recovery_point_selection,
496            tags: v.tags,
497        }
498    }
499}
500
501impl From<RestoreTestingPlanView> for RestoreTestingPlanData {
502    fn from(v: RestoreTestingPlanView) -> Self {
503        RestoreTestingPlanData {
504            restore_testing_plan_name: v.restore_testing_plan_name,
505            restore_testing_plan_arn: v.restore_testing_plan_arn,
506            schedule_expression: v.schedule_expression,
507            schedule_expression_timezone: v.schedule_expression_timezone,
508            start_window_hours: v.start_window_hours,
509            recovery_point_selection: v.recovery_point_selection,
510            creator_request_id: v.creator_request_id,
511            creation_time: parse_rfc3339_or_now(&v.creation_time),
512            last_update_time: parse_rfc3339_or_now(&v.last_update_time),
513            tags: v.tags,
514        }
515    }
516}
517
518impl From<RestoreTestingSelectionView> for RestoreTestingSelectionData {
519    fn from(v: RestoreTestingSelectionView) -> Self {
520        RestoreTestingSelectionData {
521            restore_testing_selection_name: v.restore_testing_selection_name,
522            restore_testing_plan_name: v.restore_testing_plan_name,
523            restore_testing_plan_arn: v.restore_testing_plan_arn,
524            iam_role_arn: v.iam_role_arn,
525            protected_resource_type: v.protected_resource_type,
526            protected_resource_arns: v.protected_resource_arns,
527            protected_resource_conditions: v.protected_resource_conditions,
528            restore_metadata_overrides: v.restore_metadata_overrides,
529            validation_window_hours: v.validation_window_hours,
530            creator_request_id: v.creator_request_id,
531            creation_time: parse_rfc3339_or_now(&v.creation_time),
532            last_update_time: parse_rfc3339_or_now(&v.last_update_time),
533        }
534    }
535}
536
537// ---------------------------------------------------------------------------
538// StatefulService implementation
539// ---------------------------------------------------------------------------
540
541impl StatefulService for BackupService {
542    type StateView = BackupStateView;
543
544    async fn snapshot(&self, account_id: &str, region: &str) -> Self::StateView {
545        let state = self.state.get(account_id, region);
546        let guard = state.read().await;
547        BackupStateView::from(&*guard)
548    }
549
550    async fn restore(
551        &self,
552        account_id: &str,
553        region: &str,
554        view: Self::StateView,
555    ) -> Result<(), StateViewError> {
556        use chrono::{DateTime, Utc};
557
558        let mut new_state = BackupState::default();
559
560        for (name, vv) in view.vaults {
561            let creation_date = DateTime::parse_from_rfc3339(&vv.creation_date)
562                .map(|d| d.with_timezone(&Utc))
563                .unwrap_or_else(|_| Utc::now());
564            let lock_date = vv
565                .lock_date
566                .as_deref()
567                .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
568                .map(|d| d.with_timezone(&Utc));
569            new_state.vaults.insert(
570                name,
571                BackupVault {
572                    backup_vault_name: vv.backup_vault_name,
573                    backup_vault_arn: vv.backup_vault_arn,
574                    creation_date,
575                    number_of_recovery_points: vv.number_of_recovery_points,
576                    locked: vv.locked,
577                    min_retention_days: vv.min_retention_days,
578                    max_retention_days: vv.max_retention_days,
579                    lock_date,
580                    tags: vv.tags,
581                },
582            );
583        }
584
585        for (id, pv) in view.backup_plans {
586            let creation_date = DateTime::parse_from_rfc3339(&pv.creation_date)
587                .map(|d| d.with_timezone(&Utc))
588                .unwrap_or_else(|_| Utc::now());
589            new_state.backup_plans.insert(
590                id,
591                BackupPlanData {
592                    backup_plan_id: pv.backup_plan_id,
593                    backup_plan_arn: pv.backup_plan_arn,
594                    backup_plan_name: pv.backup_plan_name,
595                    version_id: pv.version_id,
596                    creation_date,
597                    backup_plan_json: pv.backup_plan_json,
598                    tags: pv.tags,
599                },
600            );
601        }
602
603        for (name, rv) in view.report_plans {
604            let creation_time = DateTime::parse_from_rfc3339(&rv.creation_time)
605                .map(|d| d.with_timezone(&Utc))
606                .unwrap_or_else(|_| Utc::now());
607            new_state.report_plans.insert(
608                name,
609                ReportPlanData {
610                    report_plan_name: rv.report_plan_name,
611                    report_plan_arn: rv.report_plan_arn,
612                    report_plan_description: rv.report_plan_description,
613                    report_delivery_channel: rv.report_delivery_channel,
614                    report_setting: rv.report_setting,
615                    creation_time,
616                    deployment_status: rv.deployment_status,
617                    tags: rv.tags,
618                },
619            );
620        }
621
622        new_state.resource_tags = view.resource_tags;
623
624        for (k, v) in view.frameworks {
625            new_state.frameworks.insert(k, FrameworkData::from(v));
626        }
627        for (k, v) in view.vault_access_policies {
628            new_state
629                .vault_access_policies
630                .insert(k, VaultAccessPolicy::from(v));
631        }
632        for (k, v) in view.vault_notifications {
633            new_state
634                .vault_notifications
635                .insert(k, VaultNotificationConfig::from(v));
636        }
637        for (k, v) in view.tiering_configs {
638            new_state
639                .tiering_configs
640                .insert(k, TieringConfigData::from(v));
641        }
642        for (k, v) in view.legal_holds {
643            new_state.legal_holds.insert(k, LegalHoldData::from(v));
644        }
645        for (k, v) in view.restore_testing_plans {
646            new_state
647                .restore_testing_plans
648                .insert(k, RestoreTestingPlanData::from(v));
649        }
650        for (key, v) in view.restore_testing_selections {
651            // Prefer parsing the composite key, but fall back to the embedded field values.
652            let (plan, sel) = match key.split_once('/') {
653                Some((p, s)) => (p.to_string(), s.to_string()),
654                None => (
655                    v.restore_testing_plan_name.clone(),
656                    v.restore_testing_selection_name.clone(),
657                ),
658            };
659            new_state
660                .restore_testing_selections
661                .insert((plan, sel), RestoreTestingSelectionData::from(v));
662        }
663
664        {
665            let state = self.state.get(account_id, region);
666            *state.write().await = new_state;
667        }
668        self.notify_state_changed(account_id, region).await;
669        Ok(())
670    }
671
672    async fn merge(
673        &self,
674        account_id: &str,
675        region: &str,
676        view: Self::StateView,
677    ) -> Result<(), StateViewError> {
678        use chrono::{DateTime, Utc};
679
680        let state = self.state.get(account_id, region);
681        {
682            let mut guard = state.write().await;
683
684            for (name, vv) in view.vaults {
685                let creation_date = DateTime::parse_from_rfc3339(&vv.creation_date)
686                    .map(|d| d.with_timezone(&Utc))
687                    .unwrap_or_else(|_| Utc::now());
688                let lock_date = vv
689                    .lock_date
690                    .as_deref()
691                    .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
692                    .map(|d| d.with_timezone(&Utc));
693                guard.vaults.insert(
694                    name,
695                    BackupVault {
696                        backup_vault_name: vv.backup_vault_name,
697                        backup_vault_arn: vv.backup_vault_arn,
698                        creation_date,
699                        number_of_recovery_points: vv.number_of_recovery_points,
700                        locked: vv.locked,
701                        min_retention_days: vv.min_retention_days,
702                        max_retention_days: vv.max_retention_days,
703                        lock_date,
704                        tags: vv.tags,
705                    },
706                );
707            }
708
709            for (id, pv) in view.backup_plans {
710                let creation_date = DateTime::parse_from_rfc3339(&pv.creation_date)
711                    .map(|d| d.with_timezone(&Utc))
712                    .unwrap_or_else(|_| Utc::now());
713                guard.backup_plans.insert(
714                    id,
715                    BackupPlanData {
716                        backup_plan_id: pv.backup_plan_id,
717                        backup_plan_arn: pv.backup_plan_arn,
718                        backup_plan_name: pv.backup_plan_name,
719                        version_id: pv.version_id,
720                        creation_date,
721                        backup_plan_json: pv.backup_plan_json,
722                        tags: pv.tags,
723                    },
724                );
725            }
726
727            for (name, rv) in view.report_plans {
728                let creation_time = DateTime::parse_from_rfc3339(&rv.creation_time)
729                    .map(|d| d.with_timezone(&Utc))
730                    .unwrap_or_else(|_| Utc::now());
731                guard.report_plans.insert(
732                    name,
733                    ReportPlanData {
734                        report_plan_name: rv.report_plan_name,
735                        report_plan_arn: rv.report_plan_arn,
736                        report_plan_description: rv.report_plan_description,
737                        report_delivery_channel: rv.report_delivery_channel,
738                        report_setting: rv.report_setting,
739                        creation_time,
740                        deployment_status: rv.deployment_status,
741                        tags: rv.tags,
742                    },
743                );
744            }
745
746            for (arn, tags) in view.resource_tags {
747                guard.resource_tags.entry(arn).or_default().extend(tags);
748            }
749
750            for (k, v) in view.frameworks {
751                guard.frameworks.insert(k, FrameworkData::from(v));
752            }
753            for (k, v) in view.vault_access_policies {
754                guard
755                    .vault_access_policies
756                    .insert(k, VaultAccessPolicy::from(v));
757            }
758            for (k, v) in view.vault_notifications {
759                guard
760                    .vault_notifications
761                    .insert(k, VaultNotificationConfig::from(v));
762            }
763            for (k, v) in view.tiering_configs {
764                guard.tiering_configs.insert(k, TieringConfigData::from(v));
765            }
766            for (k, v) in view.legal_holds {
767                guard.legal_holds.insert(k, LegalHoldData::from(v));
768            }
769            for (k, v) in view.restore_testing_plans {
770                guard
771                    .restore_testing_plans
772                    .insert(k, RestoreTestingPlanData::from(v));
773            }
774            for (key, v) in view.restore_testing_selections {
775                let (plan, sel) = match key.split_once('/') {
776                    Some((p, s)) => (p.to_string(), s.to_string()),
777                    None => (
778                        v.restore_testing_plan_name.clone(),
779                        v.restore_testing_selection_name.clone(),
780                    ),
781                };
782                guard
783                    .restore_testing_selections
784                    .insert((plan, sel), RestoreTestingSelectionData::from(v));
785            }
786        }
787        self.notify_state_changed(account_id, region).await;
788        Ok(())
789    }
790
791    fn notifier(&self) -> &StateChangeNotifier<Self::StateView> {
792        &self.notifier
793    }
794}