1use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5
6use crate::{CallOptions, Client, ClientError, OperationWaitError, OperationWaitOptions, SubmissionDisposition};
7use lodestone_wire::{
8 CollectionExportArtifact, Digest, Operation, OperationState, PortabilityStorageCapability,
9 PrepareCollectionExportRequest, PrepareCollectionRestoreRequest, ResolveCollectionExportRequest,
10 ResolveCollectionRestoreRequest, StartCollectionExportRequest, StartCollectionRestoreRequest,
11};
12use serde::Serialize;
13
14pub const PORTABILITY_WORKFLOW_FORMAT_VERSION: u16 = 1;
15const PORTABILITY_WAIT_ATTEMPTS: u32 = 32;
16const PORTABILITY_WAIT_INTERVAL_MILLIS: u64 = 250;
17
18#[derive(Clone, Debug, PartialEq, Serialize)]
19pub struct CollectionExportExecutionV1 {
20 pub operation: Operation,
21 pub artifact: Option<CollectionExportArtifact>,
22}
23
24#[derive(Clone, Debug, PartialEq, Serialize)]
25pub struct CollectionRestoreExecutionV1 {
26 pub operation: Operation,
27}
28
29#[derive(Debug)]
30pub enum PortabilityWorkflowError {
31 Client(ClientError),
32 ExpiredUnknown,
33 MissingOperation,
34 MissingArtifact,
35 Wait(OperationWaitError),
36}
37
38impl Display for PortabilityWorkflowError {
39 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
40 formatter.write_str(match self {
41 Self::Client(_) => "Collection portability API call failed",
42 Self::ExpiredUnknown => "Collection portability outcome retired before resolution",
43 Self::MissingOperation => "Collection portability resolver omitted the operation",
44 Self::MissingArtifact => "successful Collection export omitted its artifact",
45 Self::Wait(_) => "Collection portability operation wait failed",
46 })
47 }
48}
49
50impl Error for PortabilityWorkflowError {
51 fn source(&self) -> Option<&(dyn Error + 'static)> {
52 match self {
53 Self::Client(error) => Some(error),
54 Self::Wait(error) => Some(error),
55 Self::ExpiredUnknown | Self::MissingOperation | Self::MissingArtifact => None,
56 }
57 }
58}
59
60impl From<ClientError> for PortabilityWorkflowError {
61 fn from(error: ClientError) -> Self {
62 Self::Client(error)
63 }
64}
65
66fn resolved_operation(
67 operation: Option<Operation>,
68 expired_unknown: bool,
69) -> Result<Operation, PortabilityWorkflowError> {
70 operation.ok_or(if expired_unknown {
71 PortabilityWorkflowError::ExpiredUnknown
72 } else {
73 PortabilityWorkflowError::MissingOperation
74 })
75}
76
77impl Client {
78 pub async fn export_collection(
82 &self,
83 action_id: Digest,
84 storage: PortabilityStorageCapability,
85 options: CallOptions,
86 ) -> Result<CollectionExportExecutionV1, PortabilityWorkflowError> {
87 let preparation = self
88 .prepare_collection_export(
89 PrepareCollectionExportRequest {
90 action_id: action_id.clone(),
91 storage: storage.clone(),
92 },
93 options.clone(),
94 )
95 .await?;
96 let start = self
97 .start_collection_export(
98 StartCollectionExportRequest {
99 action_id: action_id.clone(),
100 semantic_digest: preparation.semantic_digest.clone(),
101 storage: storage.clone(),
102 },
103 options.clone(),
104 )
105 .await;
106 if let Err(error) = start
107 && error.submission_disposition() == SubmissionDisposition::NotSubmitted
108 {
109 return Err(PortabilityWorkflowError::Client(error));
110 }
111 let request = ResolveCollectionExportRequest {
112 action_id,
113 semantic_digest: preparation.semantic_digest,
114 storage,
115 };
116 let mut resolved = self.resolve_collection_export(request.clone(), options.clone()).await?;
117 let mut operation = resolved_operation(resolved.operation, resolved.expired_unknown)?;
118 if operation.current_state == OperationState::Pending {
119 let _waited = self.wait_portability_operation(&operation, options.clone()).await?;
120 resolved = self.resolve_collection_export(request, options).await?;
121 operation = resolved_operation(resolved.operation, resolved.expired_unknown)?;
122 }
123 if operation.current_state == OperationState::Succeeded && resolved.artifact.is_none() {
124 return Err(PortabilityWorkflowError::MissingArtifact);
125 }
126 Ok(CollectionExportExecutionV1 {
127 operation,
128 artifact: resolved.artifact,
129 })
130 }
131
132 pub async fn restore_collection(
135 &self,
136 action_id: Digest,
137 artifact: CollectionExportArtifact,
138 storage: PortabilityStorageCapability,
139 options: CallOptions,
140 ) -> Result<CollectionRestoreExecutionV1, PortabilityWorkflowError> {
141 let preparation = self
142 .prepare_collection_restore(
143 PrepareCollectionRestoreRequest {
144 action_id: action_id.clone(),
145 artifact: artifact.clone(),
146 storage: storage.clone(),
147 },
148 options.clone(),
149 )
150 .await?;
151 let start = self
152 .start_collection_restore(
153 StartCollectionRestoreRequest {
154 action_id: action_id.clone(),
155 semantic_digest: preparation.semantic_digest.clone(),
156 artifact: artifact.clone(),
157 storage: storage.clone(),
158 },
159 options.clone(),
160 )
161 .await;
162 if let Err(error) = start
163 && error.submission_disposition() == SubmissionDisposition::NotSubmitted
164 {
165 return Err(PortabilityWorkflowError::Client(error));
166 }
167 let request = ResolveCollectionRestoreRequest {
168 action_id,
169 semantic_digest: preparation.semantic_digest,
170 artifact,
171 storage,
172 };
173 let mut resolved = self
174 .resolve_collection_restore(request.clone(), options.clone())
175 .await?;
176 let mut operation = resolved_operation(resolved.operation, resolved.expired_unknown)?;
177 if operation.current_state == OperationState::Pending {
178 let _waited = self.wait_portability_operation(&operation, options.clone()).await?;
179 resolved = self.resolve_collection_restore(request, options).await?;
180 operation = resolved_operation(resolved.operation, resolved.expired_unknown)?;
181 }
182 Ok(CollectionRestoreExecutionV1 { operation })
183 }
184
185 async fn wait_portability_operation(
186 &self,
187 operation: &Operation,
188 options: CallOptions,
189 ) -> Result<Operation, PortabilityWorkflowError> {
190 Ok(self
191 .wait_for_operation(
192 operation.locator.clone(),
193 operation.authority_revision.clone(),
194 OperationWaitOptions {
195 maximum_attempts: PORTABILITY_WAIT_ATTEMPTS,
196 interval_millis: PORTABILITY_WAIT_INTERVAL_MILLIS,
197 },
198 options,
199 )
200 .await
201 .map_err(PortabilityWorkflowError::Wait)?
202 .operation)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn portability_wait_contract_is_bounded() {
212 assert!((1..=64).contains(&PORTABILITY_WAIT_ATTEMPTS));
213 assert!((1..=60_000).contains(&PORTABILITY_WAIT_INTERVAL_MILLIS));
214 assert_eq!(PORTABILITY_WORKFLOW_FORMAT_VERSION, 1);
215 }
216}