Skip to main content

canic_backup/execution/
receipt.rs

1use super::{
2    BackupExecutionJournal, BackupExecutionJournalError, BackupExecutionJournalOperation,
3    BackupExecutionOperationReceipt, BackupExecutionOperationReceiptOutcome,
4    validation::{operation_kind_is_mutating, validate_nonempty, validate_optional_nonempty},
5};
6use crate::plan::BackupOperationKind;
7
8impl BackupExecutionOperationReceipt {
9    /// Build a completed operation receipt from one journal operation.
10    #[must_use]
11    pub fn completed(
12        journal: &BackupExecutionJournal,
13        operation: &BackupExecutionJournalOperation,
14        updated_at: Option<String>,
15    ) -> Self {
16        Self::from_operation(
17            journal,
18            operation,
19            BackupExecutionOperationReceiptOutcome::Completed,
20            updated_at,
21            None,
22        )
23    }
24
25    /// Build a failed operation receipt from one journal operation.
26    #[must_use]
27    pub fn failed(
28        journal: &BackupExecutionJournal,
29        operation: &BackupExecutionJournalOperation,
30        updated_at: Option<String>,
31        failure_reason: String,
32    ) -> Self {
33        Self::from_operation(
34            journal,
35            operation,
36            BackupExecutionOperationReceiptOutcome::Failed,
37            updated_at,
38            Some(failure_reason),
39        )
40    }
41
42    fn from_operation(
43        journal: &BackupExecutionJournal,
44        operation: &BackupExecutionJournalOperation,
45        outcome: BackupExecutionOperationReceiptOutcome,
46        updated_at: Option<String>,
47        failure_reason: Option<String>,
48    ) -> Self {
49        Self {
50            plan_id: journal.plan_id.clone(),
51            run_id: journal.run_id.clone(),
52            preflight_id: journal.preflight_id.clone(),
53            sequence: operation.sequence,
54            operation_id: operation.operation_id.clone(),
55            kind: operation.kind.clone(),
56            target_canister_id: operation.target_canister_id.clone(),
57            outcome,
58            updated_at,
59            snapshot_id: None,
60            snapshot_taken_at_timestamp: None,
61            snapshot_total_size_bytes: None,
62            artifact_path: None,
63            checksum: None,
64            failure_reason,
65        }
66    }
67
68    pub(super) fn validate_against(
69        &self,
70        journal: &BackupExecutionJournal,
71    ) -> Result<(), BackupExecutionJournalError> {
72        validate_nonempty("operation_receipts[].plan_id", &self.plan_id)?;
73        validate_nonempty("operation_receipts[].run_id", &self.run_id)?;
74        validate_nonempty("operation_receipts[].operation_id", &self.operation_id)?;
75        validate_nonempty(
76            "operation_receipts[].updated_at",
77            self.updated_at.as_deref().unwrap_or_default(),
78        )?;
79        validate_optional_nonempty(
80            "operation_receipts[].snapshot_id",
81            self.snapshot_id.as_deref(),
82        )?;
83        validate_optional_nonempty(
84            "operation_receipts[].artifact_path",
85            self.artifact_path.as_deref(),
86        )?;
87        validate_optional_nonempty("operation_receipts[].checksum", self.checksum.as_deref())?;
88
89        if self.plan_id != journal.plan_id || self.run_id != journal.run_id {
90            return Err(BackupExecutionJournalError::ReceiptJournalMismatch {
91                sequence: self.sequence,
92            });
93        }
94        let operation = journal
95            .operations
96            .iter()
97            .find(|operation| operation.sequence == self.sequence)
98            .ok_or(BackupExecutionJournalError::ReceiptOperationNotFound(
99                self.sequence,
100            ))?;
101        if operation.operation_id != self.operation_id
102            || operation.kind != self.kind
103            || operation.target_canister_id != self.target_canister_id
104        {
105            return Err(BackupExecutionJournalError::ReceiptOperationMismatch {
106                sequence: self.sequence,
107            });
108        }
109        if operation_kind_is_mutating(&operation.kind) && self.preflight_id != journal.preflight_id
110        {
111            return Err(BackupExecutionJournalError::ReceiptPreflightMismatch {
112                sequence: self.sequence,
113            });
114        }
115        if self.outcome == BackupExecutionOperationReceiptOutcome::Failed {
116            validate_nonempty(
117                "operation_receipts[].failure_reason",
118                self.failure_reason.as_deref().unwrap_or_default(),
119            )?;
120        }
121        if self.kind == BackupOperationKind::CreateSnapshot
122            && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
123        {
124            validate_nonempty(
125                "operation_receipts[].snapshot_id",
126                self.snapshot_id.as_deref().unwrap_or_default(),
127            )?;
128        }
129        if self.kind == BackupOperationKind::DownloadSnapshot
130            && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
131        {
132            validate_nonempty(
133                "operation_receipts[].artifact_path",
134                self.artifact_path.as_deref().unwrap_or_default(),
135            )?;
136        }
137        if self.kind == BackupOperationKind::VerifyArtifact
138            && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
139        {
140            validate_nonempty(
141                "operation_receipts[].checksum",
142                self.checksum.as_deref().unwrap_or_default(),
143            )?;
144        }
145
146        Ok(())
147    }
148}