lodestone-client 0.0.16

Current-only asynchronous Rust client for the Lodestone API
Documentation
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 {
    /// Performs one server-bounded operation wait under the supplied absolute
    /// call deadline. It never creates a fresh per-attempt deadline.
    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));
    }
}