Skip to main content

riffdb_client_rust/
reimport.rs

1//! Immutable operator-only reimport submissions for uncertainty-safe retry.
2
3use std::fmt;
4
5use riffdb_proto::{PublicWireError, v1, validate_public_message};
6use riffdb_types::{
7    ApplicationInstallationCampaignId, ApplicationPortabilityManifest,
8    CapabilityApplicationReimportScopeV1, ContractLineage, RequestId,
9};
10
11/// One immutable, exact application-reimport start submission.
12#[derive(Clone, Eq, PartialEq)]
13pub struct StartApplicationReimport {
14    campaign_id: ApplicationInstallationCampaignId,
15    lineage: ContractLineage,
16    scope: CapabilityApplicationReimportScopeV1,
17    portability_manifest: ApplicationPortabilityManifest,
18    canonical_export_manifest_json: Vec<u8>,
19    canonical_export_receipt_json: Vec<u8>,
20}
21
22impl StartApplicationReimport {
23    /// Retains one exact portability source under a caller-stable campaign.
24    ///
25    /// The context-free public-message validator rejects malformed or
26    /// oversized canonical documents here. The server remains authoritative
27    /// for source hashes, database identity, scope, and portability semantics.
28    pub fn new(
29        campaign_id: ApplicationInstallationCampaignId,
30        lineage: ContractLineage,
31        scope: CapabilityApplicationReimportScopeV1,
32        portability_manifest: ApplicationPortabilityManifest,
33        canonical_export_manifest_json: Vec<u8>,
34        canonical_export_receipt_json: Vec<u8>,
35    ) -> Result<Self, PublicWireError> {
36        let submission = Self {
37            campaign_id,
38            lineage,
39            scope,
40            portability_manifest,
41            canonical_export_manifest_json,
42            canonical_export_receipt_json,
43        };
44        validate_public_message(
45            &submission.request(
46                RequestId::from_bytes(campaign_id.into_bytes())
47                    .map_err(|_| PublicWireError::InvalidUuidV7)?,
48            ),
49        )?;
50        Ok(submission)
51    }
52
53    /// Caller-stable reimport campaign identity reused across retries.
54    #[must_use]
55    pub const fn campaign_id(&self) -> ApplicationInstallationCampaignId {
56        self.campaign_id
57    }
58
59    /// Exact protected application lineage.
60    #[must_use]
61    pub const fn lineage(&self) -> &ContractLineage {
62        &self.lineage
63    }
64
65    /// Exact requested Capability V7 reimport scope.
66    #[must_use]
67    pub const fn scope(&self) -> CapabilityApplicationReimportScopeV1 {
68        self.scope
69    }
70
71    /// Exact adapter-owned portability manifest.
72    #[must_use]
73    pub const fn portability_manifest(&self) -> &ApplicationPortabilityManifest {
74        &self.portability_manifest
75    }
76
77    pub(crate) fn request(&self, request_id: RequestId) -> v1::StartApplicationReimportRequest {
78        let scope = match self.scope {
79            CapabilityApplicationReimportScopeV1::PrincipalFiltered => {
80                v1::CapabilityApplicationReimportScope::PrincipalFiltered
81            }
82            CapabilityApplicationReimportScopeV1::WholeApplication => {
83                v1::CapabilityApplicationReimportScope::WholeApplication
84            }
85        };
86        v1::StartApplicationReimportRequest {
87            request_id: request_id.into_bytes().to_vec(),
88            campaign_id: self.campaign_id.into_bytes().to_vec(),
89            contract_lineage: self.lineage.as_str().to_owned(),
90            scope: scope as i32,
91            canonical_portability_manifest_json: self
92                .portability_manifest
93                .canonical_bytes()
94                .to_vec(),
95            canonical_export_manifest_json: self.canonical_export_manifest_json.clone(),
96            canonical_export_receipt_json: self.canonical_export_receipt_json.clone(),
97        }
98    }
99}
100
101impl fmt::Debug for StartApplicationReimport {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        formatter
104            .debug_struct("StartApplicationReimport")
105            .field("campaign_id", &self.campaign_id)
106            .field("lineage", &self.lineage)
107            .field("scope", &self.scope)
108            .field(
109                "portability_manifest_hash",
110                &self.portability_manifest.identity(),
111            )
112            .finish_non_exhaustive()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn uuid(seed: u8) -> [u8; 16] {
121        let mut bytes = [seed; 16];
122        bytes[6] = 0x70 | (seed & 0x0f);
123        bytes[8] = 0x80 | (seed & 0x3f);
124        bytes
125    }
126
127    #[test]
128    fn retries_change_only_outer_request_identity() {
129        let portability = ApplicationPortabilityManifest::decode_canonical(include_bytes!(
130            "../../../fixtures/export/openfga/portability-manifest-v2.json"
131        ))
132        .expect("portability manifest");
133        let start = StartApplicationReimport::new(
134            ApplicationInstallationCampaignId::from_bytes(uuid(1)).expect("campaign"),
135            portability.input().contract_lineage.clone(),
136            CapabilityApplicationReimportScopeV1::WholeApplication,
137            portability,
138            b"{\"manifest\":\"exact\"}".to_vec(),
139            b"{\"receipt\":\"exact\"}".to_vec(),
140        )
141        .expect("structural submission");
142        let first = start.request(RequestId::from_bytes(uuid(2)).expect("request"));
143        let second = start.request(RequestId::from_bytes(uuid(3)).expect("request"));
144        assert_ne!(first.request_id, second.request_id);
145        assert_eq!(first.campaign_id, second.campaign_id);
146        assert_eq!(first.contract_lineage, second.contract_lineage);
147        assert_eq!(first.scope, second.scope);
148        assert_eq!(
149            first.canonical_portability_manifest_json,
150            second.canonical_portability_manifest_json
151        );
152        assert_eq!(
153            first.canonical_export_manifest_json,
154            second.canonical_export_manifest_json
155        );
156        assert_eq!(
157            first.canonical_export_receipt_json,
158            second.canonical_export_receipt_json
159        );
160    }
161
162    #[test]
163    fn malformed_source_documents_fail_before_transport() {
164        let portability = ApplicationPortabilityManifest::decode_canonical(include_bytes!(
165            "../../../fixtures/export/openfga/portability-manifest-v2.json"
166        ))
167        .expect("portability manifest");
168        assert_eq!(
169            StartApplicationReimport::new(
170                ApplicationInstallationCampaignId::from_bytes(uuid(1)).expect("campaign"),
171                portability.input().contract_lineage.clone(),
172                CapabilityApplicationReimportScopeV1::WholeApplication,
173                portability,
174                b"not-json".to_vec(),
175                b"{}".to_vec(),
176            )
177            .expect_err("malformed source must fail"),
178            PublicWireError::InvalidBytes
179        );
180    }
181}