use std::io;
use std::time::Duration;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, EtherNetIpError>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EtherNetIpError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Protocol error: {0}")]
Protocol(String),
#[error("Tag not found: {0}")]
TagNotFound(String),
#[error("Data type mismatch: expected {expected}, got {actual}")]
DataTypeMismatch { expected: String, actual: String },
#[error("Write error: {message} (status: {status})")]
WriteError { status: u8, message: String },
#[error("Read error: {message} (status: {status})")]
ReadError { status: u8, message: String },
#[error("Invalid response: {reason}")]
InvalidResponse { reason: String },
#[error("Operation timed out after {0:?}")]
Timeout(Duration),
#[error("UDT error: {0}")]
Udt(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Connection lost: {0}")]
ConnectionLost(String),
#[error("CIP error 0x{code:02X}: {message}")]
CipError { code: u8, message: String },
#[error("String too long: max length is {max_length}, but got {actual_length}")]
StringTooLong {
max_length: usize,
actual_length: usize,
},
#[error("Invalid string: {reason}")]
InvalidString { reason: String },
#[error("Tag error: {0}")]
Tag(String),
#[error("Permission denied: {0}")]
Permission(String),
#[error("UTF-8 error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("Other error: {0}")]
Other(String),
#[error("Subscription error: {0}")]
Subscription(String),
}
impl EtherNetIpError {
#[must_use]
pub fn is_retriable(&self) -> bool {
matches!(
self,
EtherNetIpError::Timeout(_)
| EtherNetIpError::Connection(_)
| EtherNetIpError::ConnectionLost(_)
| EtherNetIpError::Io(_)
)
}
}
impl<T> From<std::sync::PoisonError<T>> for EtherNetIpError {
fn from(_: std::sync::PoisonError<T>) -> Self {
EtherNetIpError::Other("lock poisoned".to_string())
}
}
impl From<rust_ethernet_ip_tag_path::TagPathError> for EtherNetIpError {
fn from(error: rust_ethernet_ip_tag_path::TagPathError) -> Self {
EtherNetIpError::Protocol(error.to_string())
}
}
impl From<rust_ethernet_ip_protocol::ProtocolError> for EtherNetIpError {
fn from(error: rust_ethernet_ip_protocol::ProtocolError) -> Self {
EtherNetIpError::Protocol(error.to_string())
}
}
impl From<rust_ethernet_ip_types::TypeError> for EtherNetIpError {
fn from(error: rust_ethernet_ip_types::TypeError) -> Self {
EtherNetIpError::Protocol(error.to_string())
}
}
impl From<rust_ethernet_ip_udt::UdtError> for EtherNetIpError {
fn from(error: rust_ethernet_ip_udt::UdtError) -> Self {
match error {
rust_ethernet_ip_udt::UdtError::Protocol(message) => EtherNetIpError::Protocol(message),
rust_ethernet_ip_udt::UdtError::TagNotFound(tag) => EtherNetIpError::TagNotFound(tag),
rust_ethernet_ip_udt::UdtError::DataTypeMismatch { expected, actual } => {
EtherNetIpError::DataTypeMismatch { expected, actual }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
fn convert_poison_error() -> Result<()> {
let lock = Mutex::new(());
std::thread::scope(|scope| {
let handle = scope.spawn(|| {
let _guard = lock.lock().expect("test lock should not be poisoned yet");
panic!("poison test mutex");
});
assert!(handle.join().is_err());
});
let _guard = lock.lock()?;
Ok(())
}
#[test]
fn poison_error_converts_to_other_variant() {
let err = convert_poison_error().expect_err("poisoned mutex should convert into an error");
assert!(matches!(err, EtherNetIpError::Other(message) if message == "lock poisoned"));
}
}