alpine-protocol-sdk 0.2.4

High-level SDK on top of the ALPINE protocol layer.
Documentation
use alpine::handshake::HandshakeError;
use alpine::stream::StreamError;
use thiserror::Error;

use crate::transport::TransportError;

/// Errors emitted by the SDK runtime.
#[derive(Error, Debug)]
pub enum AlpineSdkError {
    #[error("transport error: {0}")]
    Transport(#[from] TransportError),

    #[error("connection timed out")]
    Timeout,

    #[error("discovery failed: {0}")]
    DiscoveryFailed(String),

    #[error("invalid discovery reply")]
    InvalidDiscoveryReply,

    #[error("handshake already in progress")]
    HandshakeAlreadyInProgress,

    #[error("discovery not allowed once handshake has begun")]
    DiscoveryAfterHandshake,

    #[error("invalid phase transition: {0}")]
    InvalidPhaseTransition(String),

    #[error("missing client_nonce")]
    MissingClientNonce,

    #[error("handshake failed: {0}")]
    HandshakeFailed(String),

    #[error("device identity verification failed")]
    IdentityVerificationFailed,

    #[error("device identity not trusted: {0}")]
    UntrustedDevice(String),

    #[error("io error: {0}")]
    Io(String),

    #[error("no active session")]
    NoActiveSession,

    #[error("session expired")]
    SessionExpired,

    #[error("streaming not supported by device")]
    StreamingNotSupported,

    #[error("invalid channel value")]
    InvalidChannelValue,

    #[error("probe failed: {0}")]
    ProbeFailed(String),

    #[error("invalid capabilities: {0}")]
    InvalidCapabilities(String),

    #[error("dangerous control command blocked (enable allow_dangerous)")]
    DangerousControlDisallowed,

    #[error("sensitive operation requires trusted identity")]
    SensitiveOperationRequiresTrust,

    #[error("vendor extension not registered: {0}")]
    VendorExtensionNotRegistered(String),

    #[error("unsupported environment: {0}")]
    UnsupportedEnvironment(String),

    #[error("incompatible protocol: {0}")]
    IncompatibleProtocol(String),

    #[error("device quarantined: {0}")]
    Quarantined(String),

    #[error("status mismatch: {0}")]
    StatusMismatch(String),

    #[error("device selection denied: {0}")]
    SelectionDenied(String),

    #[error("internal error: {0}")]
    Internal(String),
}

impl AlpineSdkError {
    pub fn user_hint(&self) -> Option<&'static str> {
        match self {
            AlpineSdkError::Timeout => Some("request timed out; check network reachability"),
            AlpineSdkError::DiscoveryFailed(_) => {
                Some("discovery failed; confirm the device is on the same network")
            }
            AlpineSdkError::HandshakeFailed(_) => {
                Some("handshake failed; verify credentials and device time")
            }
            AlpineSdkError::UntrustedDevice(_) => {
                Some("device identity not trusted; verify trust bundle")
            }
            AlpineSdkError::ProbeFailed(_) => Some("device probe failed; verify device health"),
            AlpineSdkError::DangerousControlDisallowed => {
                Some("dangerous control blocked; enable allow_dangerous to proceed")
            }
            AlpineSdkError::SensitiveOperationRequiresTrust => {
                Some("operation requires a trusted device identity")
            }
            AlpineSdkError::VendorExtensionNotRegistered(_) => {
                Some("vendor extension not registered; register before use")
            }
            AlpineSdkError::UnsupportedEnvironment(_) => {
                Some("environment not supported for UDP discovery/control")
            }
            AlpineSdkError::IncompatibleProtocol(_) => {
                Some("device protocol version is incompatible with this SDK")
            }
            AlpineSdkError::Quarantined(_) => {
                Some("device is quarantined; observe-only mode enforced")
            }
            AlpineSdkError::InvalidCapabilities(_) => {
                Some("requested capabilities are not supported by the device")
            }
            AlpineSdkError::StatusMismatch(_) => {
                Some("device does not support standard status; use vendor status helper")
            }
            AlpineSdkError::SelectionDenied(_) => Some("device rejected by selection policy"),
            _ => None,
        }
    }

    pub fn internal_cause(&self) -> Option<&str> {
        match self {
            AlpineSdkError::DiscoveryFailed(detail) => Some(detail.as_str()),
            AlpineSdkError::HandshakeFailed(detail) => Some(detail.as_str()),
            AlpineSdkError::UntrustedDevice(detail) => Some(detail.as_str()),
            AlpineSdkError::Io(detail) => Some(detail.as_str()),
            AlpineSdkError::ProbeFailed(detail) => Some(detail.as_str()),
            AlpineSdkError::InvalidCapabilities(detail) => Some(detail.as_str()),
            AlpineSdkError::VendorExtensionNotRegistered(detail) => Some(detail.as_str()),
            AlpineSdkError::UnsupportedEnvironment(detail) => Some(detail.as_str()),
            AlpineSdkError::IncompatibleProtocol(detail) => Some(detail.as_str()),
            AlpineSdkError::Quarantined(detail) => Some(detail.as_str()),
            AlpineSdkError::StatusMismatch(detail) => Some(detail.as_str()),
            AlpineSdkError::SelectionDenied(detail) => Some(detail.as_str()),
            AlpineSdkError::Internal(detail) => Some(detail.as_str()),
            _ => None,
        }
    }

    pub fn with_context(
        self,
        device_id: Option<String>,
        ip: Option<String>,
        port: Option<u16>,
        operation: Option<String>,
    ) -> SdkErrorContext {
        SdkErrorContext::with_context(self, device_id, ip, port, operation)
    }
}

#[derive(Debug)]
pub struct SdkErrorContext {
    pub error: AlpineSdkError,
    pub device_id: Option<String>,
    pub ip: Option<String>,
    pub port: Option<u16>,
    pub operation: Option<String>,
}

impl SdkErrorContext {
    pub fn new(error: AlpineSdkError) -> Self {
        Self {
            error,
            device_id: None,
            ip: None,
            port: None,
            operation: None,
        }
    }

    pub fn with_context(
        error: AlpineSdkError,
        device_id: Option<String>,
        ip: Option<String>,
        port: Option<u16>,
        operation: Option<String>,
    ) -> Self {
        Self {
            error,
            device_id,
            ip,
            port,
            operation,
        }
    }
}

impl std::fmt::Display for SdkErrorContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.error)?;
        if self.device_id.is_none()
            && self.ip.is_none()
            && self.port.is_none()
            && self.operation.is_none()
        {
            return Ok(());
        }
        write!(f, " (")?;
        let mut first = true;
        if let Some(device_id) = &self.device_id {
            write!(f, "device_id={}", device_id)?;
            first = false;
        }
        if let Some(ip) = &self.ip {
            if !first {
                write!(f, ", ")?;
            }
            write!(f, "ip={}", ip)?;
            first = false;
        }
        if let Some(port) = self.port {
            if !first {
                write!(f, ", ")?;
            }
            write!(f, "port={}", port)?;
            first = false;
        }
        if let Some(operation) = &self.operation {
            if !first {
                write!(f, ", ")?;
            }
            write!(f, "op={}", operation)?;
        }
        write!(f, ")")
    }
}

impl std::error::Error for SdkErrorContext {}

impl From<HandshakeError> for AlpineSdkError {
    fn from(err: HandshakeError) -> Self {
        match err {
            HandshakeError::Transport(_) => AlpineSdkError::HandshakeFailed(err.to_string()),
            other => AlpineSdkError::HandshakeFailed(other.to_string()),
        }
    }
}

impl From<StreamError> for AlpineSdkError {
    fn from(err: StreamError) -> Self {
        AlpineSdkError::Internal(err.to_string())
    }
}