use std::fmt;
pub type RelayResult<T> = Result<T, RelayError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelayError {
AuthenticationFailed {
reason: String,
},
RateLimitExceeded {
retry_after_ms: u64,
},
SessionError {
session_id: Option<u32>,
kind: SessionErrorKind,
},
NetworkError {
operation: String,
source: String,
},
ProtocolError {
frame_type: u8,
reason: String,
},
ResourceExhausted {
resource_type: String,
current_usage: u64,
limit: u64,
},
ConfigurationError {
parameter: String,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionErrorKind {
NotFound,
AlreadyExists,
Expired,
Terminated,
InvalidState {
current_state: String,
expected_state: String,
},
BandwidthExceeded {
used: u64,
limit: u64,
},
}
impl fmt::Display for RelayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RelayError::AuthenticationFailed { reason } => {
write!(f, "Authentication failed: {}", reason)
}
RelayError::RateLimitExceeded { retry_after_ms } => {
write!(f, "Rate limit exceeded, retry after {} ms", retry_after_ms)
}
RelayError::SessionError { session_id, kind } => match session_id {
Some(id) => write!(f, "Session {} error: {}", id, kind),
None => write!(f, "Session error: {}", kind),
},
RelayError::NetworkError { operation, source } => {
write!(f, "Network error during {}: {}", operation, source)
}
RelayError::ProtocolError { frame_type, reason } => {
write!(
f,
"Protocol error in frame 0x{:02x}: {}",
frame_type, reason
)
}
RelayError::ResourceExhausted {
resource_type,
current_usage,
limit,
} => {
write!(
f,
"Resource exhausted: {} usage ({}) exceeds limit ({})",
resource_type, current_usage, limit
)
}
RelayError::ConfigurationError { parameter, reason } => {
write!(f, "Configuration error for {}: {}", parameter, reason)
}
}
}
}
impl fmt::Display for SessionErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionErrorKind::NotFound => write!(f, "session not found"),
SessionErrorKind::AlreadyExists => write!(f, "session already exists"),
SessionErrorKind::Expired => write!(f, "session expired"),
SessionErrorKind::Terminated => write!(f, "session terminated"),
SessionErrorKind::InvalidState {
current_state,
expected_state,
} => {
write!(
f,
"invalid state '{}', expected '{}'",
current_state, expected_state
)
}
SessionErrorKind::BandwidthExceeded { used, limit } => {
write!(f, "bandwidth exceeded: {} > {}", used, limit)
}
}
}
}
impl std::error::Error for RelayError {}
impl From<std::io::Error> for RelayError {
fn from(error: std::io::Error) -> Self {
RelayError::NetworkError {
operation: "I/O operation".to_string(),
source: error.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let auth_error = RelayError::AuthenticationFailed {
reason: "Invalid signature".to_string(),
};
assert!(auth_error.to_string().contains("Authentication failed"));
let rate_limit_error = RelayError::RateLimitExceeded {
retry_after_ms: 1000,
};
assert!(rate_limit_error.to_string().contains("Rate limit exceeded"));
let session_error = RelayError::SessionError {
session_id: Some(123),
kind: SessionErrorKind::NotFound,
};
assert!(session_error.to_string().contains("Session 123 error"));
}
#[test]
fn test_session_error_kind_display() {
let invalid_state = SessionErrorKind::InvalidState {
current_state: "Connected".to_string(),
expected_state: "Idle".to_string(),
};
assert!(invalid_state.to_string().contains("invalid state"));
assert!(invalid_state.to_string().contains("Connected"));
assert!(invalid_state.to_string().contains("Idle"));
}
#[test]
fn test_error_conversion() {
let io_error =
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "Connection refused");
let relay_error: RelayError = io_error.into();
match relay_error {
RelayError::NetworkError { operation, source } => {
assert_eq!(operation, "I/O operation");
assert!(source.contains("Connection refused"));
}
_ => panic!("Expected NetworkError"),
}
}
#[test]
fn relay_error_display_covers_all_variants() {
let cases = [
(
RelayError::AuthenticationFailed {
reason: "bad token".to_string(),
},
"Authentication failed: bad token",
),
(
RelayError::RateLimitExceeded {
retry_after_ms: 250,
},
"Rate limit exceeded, retry after 250 ms",
),
(
RelayError::SessionError {
session_id: None,
kind: SessionErrorKind::Expired,
},
"Session error: session expired",
),
(
RelayError::NetworkError {
operation: "dial".to_string(),
source: "timeout".to_string(),
},
"Network error during dial: timeout",
),
(
RelayError::ProtocolError {
frame_type: 0xab,
reason: "reserved".to_string(),
},
"Protocol error in frame 0xab: reserved",
),
(
RelayError::ResourceExhausted {
resource_type: "sessions".to_string(),
current_usage: 11,
limit: 10,
},
"Resource exhausted: sessions usage (11) exceeds limit (10)",
),
(
RelayError::ConfigurationError {
parameter: "port".to_string(),
reason: "out of range".to_string(),
},
"Configuration error for port: out of range",
),
];
for (error, expected) in cases {
assert_eq!(error.to_string(), expected);
}
}
#[test]
fn session_error_kind_display_covers_all_variants() {
let cases = [
(SessionErrorKind::NotFound, "session not found"),
(SessionErrorKind::AlreadyExists, "session already exists"),
(SessionErrorKind::Expired, "session expired"),
(SessionErrorKind::Terminated, "session terminated"),
(
SessionErrorKind::InvalidState {
current_state: "open".to_string(),
expected_state: "closed".to_string(),
},
"invalid state 'open', expected 'closed'",
),
(
SessionErrorKind::BandwidthExceeded {
used: 1025,
limit: 1024,
},
"bandwidth exceeded: 1025 > 1024",
),
];
for (kind, expected) in cases {
assert_eq!(kind.to_string(), expected);
}
}
#[test]
fn relay_error_clone_and_equality_preserve_payloads() {
let error = RelayError::SessionError {
session_id: Some(7),
kind: SessionErrorKind::BandwidthExceeded {
used: 2048,
limit: 1024,
},
};
let cloned = error.clone();
assert_eq!(cloned, error);
assert!(format!("{cloned:?}").contains("BandwidthExceeded"));
}
#[test]
fn relay_result_alias_accepts_relay_error() {
fn failing() -> RelayResult<()> {
Err(RelayError::ConfigurationError {
parameter: "relay.enabled".to_string(),
reason: "disabled".to_string(),
})
}
let error = failing().expect_err("expected relay error");
assert_eq!(
error.to_string(),
"Configuration error for relay.enabled: disabled"
);
}
#[test]
fn relay_error_implements_std_error() {
fn as_error(error: &dyn std::error::Error) -> String {
error.to_string()
}
let error = RelayError::ProtocolError {
frame_type: 0x01,
reason: "too short".to_string(),
};
assert_eq!(as_error(&error), "Protocol error in frame 0x01: too short");
}
}