riffdb_client_rust/
installation.rs1use std::fmt;
4
5use riffdb_proto::v1;
6use riffdb_types::{
7 ApplicationInstallationCampaignId, ApplicationInstallationPlan, InstallationCampaignError,
8 InstallationDriver, InstalledSeedEvidence, RequestId,
9};
10
11#[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 #[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 #[must_use]
41 pub const fn campaign_id(&self) -> ApplicationInstallationCampaignId {
42 self.campaign_id
43 }
44
45 #[must_use]
47 pub const fn plan(&self) -> &ApplicationInstallationPlan {
48 &self.plan
49 }
50
51 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 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}