iot-core 0.0.1

Core types for the iot-protocols SDK: Thing model, IotClient trait, errors, paths, protocol bindings.
Documentation
//! Unified error type returned across the SDK.
//!
//! Each protocol crate wraps its own internal error in the matching
//! variant of [`IotError`] via `From` impls — those impls are gated by
//! workspace features so the umbrella crate only compiles in the variants
//! the user actually opted into.
//!
//! The transport-layer and codec-layer errors are protocol-agnostic and so
//! they live here unconditionally.

use core::fmt;
use smol_str::SmolStr;

use crate::path::PropertyPath;

/// Convenience alias for SDK results.
pub type IotResult<T> = core::result::Result<T, IotError>;

/// Errors raised by transports (connect / read / write / close).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportError {
    /// Underlying socket / serial port not connected.
    NotConnected,
    /// Address parse / DNS resolution failure. Carries a human-readable
    /// reason; we do not embed `std::io::Error` here so the type stays
    /// `Eq + Clone + no_std`.
    AddressInvalid(SmolStr),
    /// Failed during connect.
    ConnectFailed(SmolStr),
    /// I/O failure — generic. The protocol layer maps this to
    /// `IotError::Transport`.
    Io(SmolStr),
    /// Operation timed out.
    Timeout,
    /// TLS / DTLS handshake failure.
    Tls(SmolStr),
    /// Serial-port specific: framing / parity error.
    SerialFraming(SmolStr),
    /// Peer closed the connection unexpectedly.
    Closed,
}

impl fmt::Display for TransportError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use TransportError::*;
        match self {
            NotConnected => write!(f, "transport not connected"),
            AddressInvalid(s) => write!(f, "invalid address: {s}"),
            ConnectFailed(s) => write!(f, "connect failed: {s}"),
            Io(s) => write!(f, "io error: {s}"),
            Timeout => write!(f, "operation timed out"),
            Tls(s) => write!(f, "tls error: {s}"),
            SerialFraming(s) => write!(f, "serial framing error: {s}"),
            Closed => write!(f, "connection closed"),
        }
    }
}

impl core::error::Error for TransportError {}

/// Errors raised by protocol codecs (encode / decode of a frame).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecError {
    /// Buffer truncated mid-frame.
    UnexpectedEof,
    /// Frame larger than the configured max.
    FrameTooLarge {
        /// Maximum frame size the codec was configured to accept.
        max: u32,
        /// Actual size declared in the wire header.
        actual: u32,
    },
    /// Reserved field had a non-zero value, or a flag combination is
    /// disallowed by the spec.
    ProtocolViolation(SmolStr),
    /// CRC / LRC / checksum mismatch.
    ChecksumMismatch,
    /// Wire-format string was not valid utf-8 / ascii.
    InvalidString(SmolStr),
    /// A value the codec produced fits the wire shape but cannot be
    /// represented in the Rust target type (e.g. negative length).
    ValueOutOfRange(SmolStr),
}

impl fmt::Display for CodecError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use CodecError::*;
        match self {
            UnexpectedEof => write!(f, "unexpected end of buffer while decoding"),
            FrameTooLarge { max, actual } => {
                write!(f, "frame too large: {actual} bytes, max {max}")
            }
            ProtocolViolation(s) => write!(f, "protocol violation: {s}"),
            ChecksumMismatch => write!(f, "checksum mismatch"),
            InvalidString(s) => write!(f, "invalid string: {s}"),
            ValueOutOfRange(s) => write!(f, "value out of range: {s}"),
        }
    }
}

impl core::error::Error for CodecError {}

/// Top-level error every public API in the SDK returns.
///
/// Protocol-specific variants are intentionally *not* enumerated here — they
/// live in their own crates and are wrapped via the protocol crate's own
/// error type (which itself wraps `CodecError` / `TransportError` for the
/// shared sub-failures). The conversion from a protocol-specific error to
/// `IotError` happens at the boundary between the protocol crate and the
/// gateway / IotClient layer.
#[derive(Debug)]
#[non_exhaustive]
pub enum IotError {
    /// Operation requested while the client is not connected.
    NotConnected,
    /// Connection establishment failed.
    ConnectionFailed(SmolStr),
    /// Authentication failed (bad credentials, expired cert, ...).
    AuthFailed(SmolStr),
    /// Operation timed out (any layer).
    Timeout,
    /// The target [`PropertyPath`] is not in the active mapping.
    PathNotBound(PropertyPath),
    /// The path is bound but the binding kind does not match the protocol
    /// (e.g. asking the MQTT client to handle a Modbus binding).
    PathKindMismatch {
        /// The path that triggered the mismatch.
        path: PropertyPath,
        /// Human-readable description of the expected binding variant.
        expected: &'static str,
    },
    /// Codec error shared with the protocol layer.
    Codec(CodecError),
    /// Transport error shared with the protocol layer.
    Transport(TransportError),
    /// Catch-all for protocol-specific failures.
    Protocol(SmolStr),
}

impl fmt::Display for IotError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use IotError::*;
        match self {
            NotConnected => write!(f, "client not connected"),
            ConnectionFailed(s) => write!(f, "connection failed: {s}"),
            AuthFailed(s) => write!(f, "authentication failed: {s}"),
            Timeout => write!(f, "operation timed out"),
            PathNotBound(p) => write!(f, "path not bound: {p}"),
            PathKindMismatch { path, expected } => {
                write!(f, "path {path} is not a {expected} binding")
            }
            Codec(e) => write!(f, "codec error: {e}"),
            Transport(e) => write!(f, "transport error: {e}"),
            Protocol(s) => write!(f, "protocol error: {s}"),
        }
    }
}

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

impl From<CodecError> for IotError {
    fn from(e: CodecError) -> Self {
        Self::Codec(e)
    }
}
impl From<TransportError> for IotError {
    fn from(e: TransportError) -> Self {
        Self::Transport(e)
    }
}

// ---- bridge to `embedded_io_async::Error` --------------------------------
//
// The `iot-transport` crate's byte-level traits are aliases for
// `embedded_io_async::{Read,Write}`, which require the associated `Error`
// type to implement `embedded_io_async::Error`. We map TransportError onto
// the closest ErrorKind variant — protocol crates that need finer detail
// can downcast back to TransportError via the AsyncSocket adapter.
impl embedded_io_async::Error for TransportError {
    fn kind(&self) -> embedded_io_async::ErrorKind {
        use embedded_io_async::ErrorKind as K;
        match self {
            TransportError::NotConnected => K::NotConnected,
            TransportError::AddressInvalid(_) => K::AddrNotAvailable,
            TransportError::ConnectFailed(_) => K::ConnectionRefused,
            TransportError::Io(_) => K::Other,
            TransportError::Timeout => K::TimedOut,
            TransportError::Tls(_) => K::PermissionDenied,
            TransportError::SerialFraming(_) => K::InvalidData,
            TransportError::Closed => K::ConnectionAborted,
        }
    }
}