use lodestone_wire::{
LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS, LODESTONE_API_MAX_OPERATION_WAIT_INTERVAL_MILLIS, Locator, Operation,
OperationState, U64, WaitOperationRequest,
};
use crate::{CallOptions, Client, ClientError};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OperationWaitOptions {
pub maximum_attempts: u32,
pub interval_millis: u64,
}
impl Default for OperationWaitOptions {
fn default() -> Self {
Self {
maximum_attempts: 20,
interval_millis: 100,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct OperationWaitOutcome {
pub operation: Operation,
pub attempts: u32,
pub terminal: bool,
}
#[derive(Debug)]
pub enum OperationWaitError {
InvalidOptions,
InvalidAuthorityRevision,
Client(ClientError),
}
impl Client {
pub async fn wait_for_operation(
&self,
locator: Locator,
after_authority_revision: U64,
wait: OperationWaitOptions,
options: CallOptions,
) -> Result<OperationWaitOutcome, OperationWaitError> {
validate_wait_options(wait)?;
if after_authority_revision.validate().is_err() {
return Err(OperationWaitError::InvalidAuthorityRevision);
}
let result = self
.wait_operation(
WaitOperationRequest {
locator,
after_authority_revision,
maximum_attempts: wait.maximum_attempts,
interval_millis: U64 {
value: wait.interval_millis.to_string(),
},
},
options,
)
.await
.map_err(OperationWaitError::Client)?;
let terminal = operation_is_terminal(result.operation.current_state);
Ok(OperationWaitOutcome {
operation: result.operation,
attempts: result.attempts,
terminal,
})
}
}
fn validate_wait_options(options: OperationWaitOptions) -> Result<(), OperationWaitError> {
if options.maximum_attempts == 0
|| options.maximum_attempts > LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS
|| options.interval_millis == 0
|| options.interval_millis > LODESTONE_API_MAX_OPERATION_WAIT_INTERVAL_MILLIS
{
Err(OperationWaitError::InvalidOptions)
} else {
Ok(())
}
}
const fn operation_is_terminal(state: OperationState) -> bool {
!matches!(state, OperationState::Pending)
}
impl std::fmt::Display for OperationWaitError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::InvalidOptions => "operation wait options exceed the native bounded contract",
Self::InvalidAuthorityRevision => "operation wait authority revision must be a canonical u64",
Self::Client(_) => "operation wait request failed",
})
}
}
impl std::error::Error for OperationWaitError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn operation_wait_options_are_bounded_by_native_limits() {
assert!(validate_wait_options(OperationWaitOptions::default()).is_ok());
assert!(matches!(
validate_wait_options(OperationWaitOptions {
maximum_attempts: LODESTONE_API_MAX_OPERATION_WAIT_ATTEMPTS.saturating_add(1),
interval_millis: 1,
}),
Err(OperationWaitError::InvalidOptions)
));
assert!(matches!(
validate_wait_options(OperationWaitOptions {
maximum_attempts: 1,
interval_millis: 0,
}),
Err(OperationWaitError::InvalidOptions)
));
}
#[test]
fn operation_wait_accepts_the_wire_zero_revision_and_classifies_terminal_state() {
assert!(U64 { value: "0".to_owned() }.validate().is_ok());
assert!(!operation_is_terminal(OperationState::Pending));
assert!(operation_is_terminal(OperationState::Succeeded));
}
}