Skip to main content

riffdb_client_rust/
export.rs

1//! Immutable symbolic export submissions for uncertainty-safe retry.
2
3use std::fmt;
4
5use riffdb_proto::v1;
6use riffdb_types::{
7    ApplicationExportOperationId, ApplicationExportSelectionV1, ApplicationPortabilityManifest,
8    CapabilityApplicationExportScopeV1, RequestId,
9};
10
11/// One immutable exact application-export start or replay submission.
12#[derive(Clone, Eq, PartialEq)]
13pub struct StartApplicationExport {
14    operation_id: ApplicationExportOperationId,
15    selection: ApplicationExportSelectionV1,
16    portability_manifest: Option<ApplicationPortabilityManifest>,
17    lease_seconds: u32,
18}
19
20impl StartApplicationExport {
21    /// Retains one closed selection under a caller-stable operation identity.
22    #[must_use]
23    pub const fn new(
24        operation_id: ApplicationExportOperationId,
25        selection: ApplicationExportSelectionV1,
26        lease_seconds: u32,
27    ) -> Self {
28        Self {
29            operation_id,
30            selection,
31            portability_manifest: None,
32            lease_seconds,
33        }
34    }
35
36    /// Retains one exact portability manifest as part of the retry identity.
37    #[must_use]
38    pub fn new_portability(
39        operation_id: ApplicationExportOperationId,
40        selection: ApplicationExportSelectionV1,
41        portability_manifest: ApplicationPortabilityManifest,
42        lease_seconds: u32,
43    ) -> Self {
44        Self {
45            operation_id,
46            selection,
47            portability_manifest: Some(portability_manifest),
48            lease_seconds,
49        }
50    }
51
52    /// Caller-stable identity reused across uncertain transport retries.
53    #[must_use]
54    pub const fn operation_id(&self) -> ApplicationExportOperationId {
55        self.operation_id
56    }
57
58    /// Exact symbolic lineage, scope, and record classes.
59    #[must_use]
60    pub const fn selection(&self) -> &ApplicationExportSelectionV1 {
61        &self.selection
62    }
63
64    /// Requested bounded server-owned lease in seconds.
65    #[must_use]
66    pub const fn lease_seconds(&self) -> u32 {
67        self.lease_seconds
68    }
69
70    /// Exact portability manifest, absent for an ordinary export.
71    #[must_use]
72    pub const fn portability_manifest(&self) -> Option<&ApplicationPortabilityManifest> {
73        self.portability_manifest.as_ref()
74    }
75
76    pub(crate) fn request(&self, request_id: RequestId) -> v1::StartApplicationExportRequest {
77        let scope = match self.selection.scope() {
78            CapabilityApplicationExportScopeV1::PrincipalFiltered => {
79                v1::CapabilityApplicationExportScope::PrincipalFiltered
80            }
81            CapabilityApplicationExportScopeV1::WholeApplication => {
82                v1::CapabilityApplicationExportScope::WholeApplication
83            }
84        };
85        v1::StartApplicationExportRequest {
86            request_id: request_id.into_bytes().to_vec(),
87            operation_id: self.operation_id.into_bytes().to_vec(),
88            selection: Some(v1::ApplicationExportSelection {
89                contract_lineage: self.selection.lineage().as_str().to_owned(),
90                scope: scope as i32,
91                entities: self.selection.entities(),
92                events: self.selection.events(),
93                provenance: self.selection.provenance(),
94                public_audit: self.selection.public_audit(),
95            }),
96            lease_seconds: self.lease_seconds,
97            canonical_portability_manifest_json: self
98                .portability_manifest
99                .as_ref()
100                .map_or_else(Vec::new, |manifest| manifest.canonical_bytes().to_vec()),
101        }
102    }
103}
104
105impl fmt::Debug for StartApplicationExport {
106    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107        formatter
108            .debug_struct("StartApplicationExport")
109            .field("operation_id", &self.operation_id)
110            .field("selection", &self.selection)
111            .field(
112                "portability_manifest",
113                &self
114                    .portability_manifest
115                    .as_ref()
116                    .map(|value| value.identity()),
117            )
118            .field("lease_seconds", &self.lease_seconds)
119            .finish()
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use riffdb_types::ContractLineage;
126
127    use super::*;
128
129    fn uuid(seed: u8) -> [u8; 16] {
130        let mut bytes = [seed; 16];
131        bytes[6] = 0x70 | (seed & 0x0f);
132        bytes[8] = 0x80 | (seed & 0x3f);
133        bytes
134    }
135
136    #[test]
137    fn retry_changes_only_outer_request_identity() {
138        let start = StartApplicationExport::new(
139            ApplicationExportOperationId::from_bytes(uuid(1)).expect("operation"),
140            ApplicationExportSelectionV1::new(
141                ContractLineage::new("TicketDesk").expect("lineage"),
142                CapabilityApplicationExportScopeV1::WholeApplication,
143                true,
144                true,
145                true,
146                false,
147            )
148            .expect("selection"),
149            900,
150        );
151        let first = start.request(RequestId::from_bytes(uuid(2)).expect("request"));
152        let second = start.request(RequestId::from_bytes(uuid(3)).expect("request"));
153        assert_ne!(first.request_id, second.request_id);
154        assert_eq!(first.operation_id, second.operation_id);
155        assert_eq!(first.selection, second.selection);
156        assert_eq!(first.lease_seconds, second.lease_seconds);
157        assert!(first.canonical_portability_manifest_json.is_empty());
158    }
159
160    #[test]
161    fn portability_manifest_bytes_are_part_of_every_retry_submission() {
162        let manifest = ApplicationPortabilityManifest::decode_canonical(include_bytes!(
163            "../../../fixtures/export/openfga/portability-manifest-v2.json"
164        ))
165        .expect("manifest");
166        let selection = ApplicationExportSelectionV1::new(
167            manifest.input().contract_lineage.clone(),
168            CapabilityApplicationExportScopeV1::WholeApplication,
169            true,
170            false,
171            false,
172            false,
173        )
174        .expect("selection");
175        let start = StartApplicationExport::new_portability(
176            ApplicationExportOperationId::from_bytes(uuid(1)).expect("operation"),
177            selection,
178            manifest.clone(),
179            900,
180        );
181        let first = start.request(RequestId::from_bytes(uuid(2)).expect("request"));
182        let second = start.request(RequestId::from_bytes(uuid(3)).expect("request"));
183        assert_eq!(
184            first.canonical_portability_manifest_json,
185            manifest.canonical_bytes()
186        );
187        assert_eq!(
188            first.canonical_portability_manifest_json,
189            second.canonical_portability_manifest_json
190        );
191    }
192}