zkteco 1.0.0

A pure-Rust client for ZKTeco biometric attendance / access-control terminals (TCP/UDP protocol). A faithful port of the Python `pyzk` library.
Documentation
//! Error type for all fallible operations in this crate.
//!
//! Mirrors `pyzk`'s exception hierarchy (`ZKError`, `ZKErrorConnection`,
//! `ZKErrorResponse`, `ZKNetworkError`) as a single Rust enum.

use std::fmt;

/// Convenience alias for results returned by this crate.
pub type ZkResult<T> = Result<T, ZkError>;

/// Errors that can occur while talking to a ZKTeco device.
#[derive(Debug)]
#[non_exhaustive]
pub enum ZkError {
    /// A command was attempted on an instance that is not connected.
    NotConnected,

    /// The device replied, but with a failure/non-acknowledgement code.
    ///
    /// The string describes the operation that failed; the optional code is the
    /// raw reply command returned by the device, when known.
    Response {
        /// Human-readable description of which operation failed.
        message: String,
        /// The raw reply code from the device, if available.
        code: Option<u16>,
    },

    /// The device requires a password and authentication failed.
    Unauthenticated,

    /// A feature is not supported by this device/firmware.
    NotSupported(String),

    /// A low-level network/socket error (connect, send, receive, timeout).
    Network(std::io::Error),

    /// A malformed or unexpected response could not be parsed.
    Parse(String),
}

impl ZkError {
    /// Build a [`ZkError::Response`] with no associated device code.
    pub(crate) fn response(message: impl Into<String>) -> Self {
        ZkError::Response {
            message: message.into(),
            code: None,
        }
    }

    /// Build a [`ZkError::Response`] carrying the raw device reply code.
    pub(crate) fn response_with_code(message: impl Into<String>, code: u16) -> Self {
        ZkError::Response {
            message: message.into(),
            code: Some(code),
        }
    }
}

impl fmt::Display for ZkError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ZkError::NotConnected => write!(f, "instance is not connected"),
            ZkError::Response { message, code } => match code {
                Some(c) => write!(f, "{message} (device code {c})"),
                None => write!(f, "{message}"),
            },
            ZkError::Unauthenticated => write!(f, "unauthenticated: wrong device password"),
            ZkError::NotSupported(what) => write!(f, "not supported: {what}"),
            ZkError::Network(e) => write!(f, "network error: {e}"),
            ZkError::Parse(e) => write!(f, "parse error: {e}"),
        }
    }
}

impl std::error::Error for ZkError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ZkError::Network(e) => Some(e),
            _ => None,
        }
    }
}

impl From<std::io::Error> for ZkError {
    fn from(e: std::io::Error) -> Self {
        ZkError::Network(e)
    }
}