Skip to main content

riffdb_client_rust/
installation.rs

1//! Immutable exact installation submissions for uncertainty-safe retry.
2
3use std::fmt;
4
5use riffdb_proto::v1;
6use riffdb_types::{
7    ApplicationInstallationCampaignId, ApplicationInstallationPlan, InstallationCampaignError,
8    InstallationDriver, InstalledSeedEvidence, RequestId,
9};
10
11/// One immutable exact application-installation start or resume submission.
12#[derive(Clone)]
13pub struct StartApplicationInstallation {
14    campaign_id: ApplicationInstallationCampaignId,
15    plan: ApplicationInstallationPlan,
16    external_completion: Option<ExternalInstallationCompletion>,
17}
18
19#[derive(Clone)]
20enum ExternalInstallationCompletion {
21    DriverProof(Vec<InstallationDriver>),
22    Seeds(Vec<InstalledSeedEvidence>),
23}
24
25impl StartApplicationInstallation {
26    /// Retains the canonical plan under one caller-stable campaign identity.
27    #[must_use]
28    pub const fn new(
29        campaign_id: ApplicationInstallationCampaignId,
30        plan: ApplicationInstallationPlan,
31    ) -> Self {
32        Self {
33            campaign_id,
34            plan,
35            external_completion: None,
36        }
37    }
38
39    /// Caller-stable campaign identity used across retries and observation.
40    #[must_use]
41    pub const fn campaign_id(&self) -> ApplicationInstallationCampaignId {
42        self.campaign_id
43    }
44
45    /// Exact content-addressed canonical plan.
46    #[must_use]
47    pub const fn plan(&self) -> &ApplicationInstallationPlan {
48        &self.plan
49    }
50
51    /// Attaches proof that every plan-declared public driver observed the
52    /// installed identity through its first-party path.
53    pub fn with_driver_proof(
54        mut self,
55        drivers: Vec<InstallationDriver>,
56    ) -> Result<Self, InstallationCampaignError> {
57        self.plan.validate_driver_proof(&drivers)?;
58        self.external_completion = Some(ExternalInstallationCompletion::DriverProof(drivers));
59        Ok(self)
60    }
61
62    /// Attaches bounded terminal counters from the plan-declared ordinary
63    /// command seed batches.
64    pub fn with_seed_receipts(
65        mut self,
66        seeds: Vec<InstalledSeedEvidence>,
67    ) -> Result<Self, InstallationCampaignError> {
68        self.plan.validate_seed_receipts(&seeds)?;
69        self.external_completion = Some(ExternalInstallationCompletion::Seeds(seeds));
70        Ok(self)
71    }
72
73    pub(crate) fn request(&self, request_id: RequestId) -> v1::StartApplicationInstallationRequest {
74        v1::StartApplicationInstallationRequest {
75            request_id: request_id.into_bytes().to_vec(),
76            campaign_id: self.campaign_id.into_bytes().to_vec(),
77            canonical_plan: self.plan.canonical_bytes().to_vec(),
78            external_completion: self
79                .external_completion
80                .as_ref()
81                .map(external_completion_to_proto),
82        }
83    }
84}
85
86fn external_completion_to_proto(
87    completion: &ExternalInstallationCompletion,
88) -> v1::ApplicationInstallationExternalCompletion {
89    use v1::application_installation_external_completion::Completion;
90
91    let completion = match completion {
92        ExternalInstallationCompletion::DriverProof(drivers) => {
93            Completion::DriverProof(v1::ApplicationInstallationDriverProof {
94                drivers: drivers
95                    .iter()
96                    .map(|driver| match driver {
97                        InstallationDriver::Rust => v1::ApplicationInstallationDriver::Rust,
98                        InstallationDriver::TypeScript => {
99                            v1::ApplicationInstallationDriver::Typescript
100                        }
101                        InstallationDriver::Go => v1::ApplicationInstallationDriver::Go,
102                        InstallationDriver::Python => v1::ApplicationInstallationDriver::Python,
103                    } as i32)
104                    .collect(),
105            })
106        }
107        ExternalInstallationCompletion::Seeds(seeds) => {
108            Completion::SeedReceipts(v1::ApplicationInstallationSeedReceipts {
109                seeds: seeds
110                    .iter()
111                    .map(|seed| v1::ApplicationInstallationSeedReceipt {
112                        name: seed.name().as_str().to_owned(),
113                        content_hash: seed.content_hash().as_bytes().to_vec(),
114                        succeeded: seed.succeeded(),
115                        replayed: seed.replayed(),
116                    })
117                    .collect(),
118            })
119        }
120    };
121    v1::ApplicationInstallationExternalCompletion {
122        completion: Some(completion),
123    }
124}
125
126impl fmt::Debug for StartApplicationInstallation {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter
129            .debug_struct("StartApplicationInstallation")
130            .field("campaign_id", &self.campaign_id)
131            .field("plan_hash", &self.plan.identity())
132            .finish_non_exhaustive()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    fn uuid(seed: u8) -> [u8; 16] {
140        let mut bytes = [seed; 16];
141        bytes[6] = 0x70 | (seed & 0x0f);
142        bytes[8] = 0x80 | (seed & 0x3f);
143        bytes
144    }
145
146    fn plan() -> ApplicationInstallationPlan {
147        ApplicationInstallationPlan::decode_canonical(include_bytes!(
148            "../../../fixtures/installation/application-installation-plan-v1.json"
149        ))
150        .expect("plan fixture")
151    }
152
153    #[test]
154    fn retries_change_only_the_transport_request_id() {
155        let start = StartApplicationInstallation::new(
156            ApplicationInstallationCampaignId::from_bytes(uuid(1)).expect("campaign"),
157            plan(),
158        );
159        let first = start.request(RequestId::from_bytes(uuid(2)).expect("request"));
160        let second = start.request(RequestId::from_bytes(uuid(3)).expect("request"));
161        assert_ne!(first.request_id, second.request_id);
162        assert_eq!(first.campaign_id, second.campaign_id);
163        assert_eq!(first.canonical_plan, second.canonical_plan);
164    }
165
166    #[test]
167    fn external_completion_is_limited_to_exact_driver_or_seed_evidence() {
168        let start = StartApplicationInstallation::new(
169            ApplicationInstallationCampaignId::from_bytes(uuid(4)).expect("campaign"),
170            plan(),
171        )
172        .with_driver_proof(vec![
173            InstallationDriver::Rust,
174            InstallationDriver::TypeScript,
175        ])
176        .expect("exact driver proof");
177        let request = start.request(RequestId::from_bytes(uuid(5)).expect("request"));
178        assert!(matches!(
179            request.external_completion,
180            Some(v1::ApplicationInstallationExternalCompletion {
181                completion: Some(
182                    v1::application_installation_external_completion::Completion::DriverProof(_)
183                )
184            })
185        ));
186    }
187}