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            artifact_path: None,
61            checksum: None,
62            failure_reason,
63        }
64    }
65
66    pub(super) fn validate_against(
67        &self,
68        journal: &BackupExecutionJournal,
69    ) -> Result<(), BackupExecutionJournalError> {
70        validate_nonempty("operation_receipts[].plan_id", &self.plan_id)?;
71        validate_nonempty("operation_receipts[].run_id", &self.run_id)?;
72        validate_nonempty("operation_receipts[].operation_id", &self.operation_id)?;
73        validate_optional_nonempty(
74            "operation_receipts[].updated_at",
75            self.updated_at.as_deref(),
76        )?;
77        validate_optional_nonempty(
78            "operation_receipts[].snapshot_id",
79            self.snapshot_id.as_deref(),
80        )?;
81        validate_optional_nonempty(
82            "operation_receipts[].artifact_path",
83            self.artifact_path.as_deref(),
84        )?;
85        validate_optional_nonempty("operation_receipts[].checksum", self.checksum.as_deref())?;
86
87        if self.plan_id != journal.plan_id || self.run_id != journal.run_id {
88            return Err(BackupExecutionJournalError::ReceiptJournalMismatch {
89                sequence: self.sequence,
90            });
91        }
92        let operation = journal
93            .operations
94            .iter()
95            .find(|operation| operation.sequence == self.sequence)
96            .ok_or(BackupExecutionJournalError::ReceiptOperationNotFound(
97                self.sequence,
98            ))?;
99        if operation.operation_id != self.operation_id
100            || operation.kind != self.kind
101            || operation.target_canister_id != self.target_canister_id
102        {
103            return Err(BackupExecutionJournalError::ReceiptOperationMismatch {
104                sequence: self.sequence,
105            });
106        }
107        if operation_kind_is_mutating(&operation.kind) && self.preflight_id != journal.preflight_id
108        {
109            return Err(BackupExecutionJournalError::ReceiptPreflightMismatch {
110                sequence: self.sequence,
111            });
112        }
113        if self.outcome == BackupExecutionOperationReceiptOutcome::Failed {
114            validate_nonempty(
115                "operation_receipts[].failure_reason",
116                self.failure_reason.as_deref().unwrap_or_default(),
117            )?;
118        }
119        if self.kind == BackupOperationKind::CreateSnapshot
120            && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
121        {
122            validate_nonempty(
123                "operation_receipts[].snapshot_id",
124                self.snapshot_id.as_deref().unwrap_or_default(),
125            )?;
126        }
127        if self.kind == BackupOperationKind::DownloadSnapshot
128            && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
129        {
130            validate_nonempty(
131                "operation_receipts[].artifact_path",
132                self.artifact_path.as_deref().unwrap_or_default(),
133            )?;
134        }
135        if self.kind == BackupOperationKind::VerifyArtifact
136            && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
137        {
138            validate_nonempty(
139                "operation_receipts[].checksum",
140                self.checksum.as_deref().unwrap_or_default(),
141            )?;
142        }
143
144        Ok(())
145    }
146}