use std::fmt;
use std::net::SocketAddr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportResult<T> {
Success(T),
Unsupported,
TemporarilyUnavailable,
Failed(TransportError),
}
impl<T> TransportResult<T> {
pub fn is_success(&self) -> bool {
matches!(self, Self::Success(_))
}
pub fn is_unsupported(&self) -> bool {
matches!(self, Self::Unsupported)
}
pub fn should_retry(&self) -> bool {
matches!(self, Self::TemporarilyUnavailable)
}
pub fn into_option(self) -> Option<T> {
match self {
Self::Success(v) => Some(v),
_ => None,
}
}
pub fn into_result(self) -> Result<Option<T>, TransportError> {
match self {
Self::Success(v) => Ok(Some(v)),
Self::Unsupported => Ok(None),
Self::TemporarilyUnavailable => Ok(None),
Self::Failed(e) => Err(e),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransportError {
pub kind: TransportErrorKind,
pub message: String,
}
impl TransportError {
pub fn new(kind: TransportErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
pub fn connection_refused(addr: SocketAddr) -> Self {
Self::new(
TransportErrorKind::ConnectionRefused,
format!("Connection refused to {}", addr),
)
}
pub fn timeout(addr: SocketAddr) -> Self {
Self::new(
TransportErrorKind::Timeout,
format!("Connection to {} timed out", addr),
)
}
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.kind, self.message)
}
}
impl std::error::Error for TransportError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportErrorKind {
ConnectionRefused,
Timeout,
NetworkUnreachable,
HostUnreachable,
AddressNotAvailable,
PermissionDenied,
ProtocolNotSupported,
Io,
}
impl fmt::Display for TransportErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ConnectionRefused => write!(f, "connection refused"),
Self::Timeout => write!(f, "timeout"),
Self::NetworkUnreachable => write!(f, "network unreachable"),
Self::HostUnreachable => write!(f, "host unreachable"),
Self::AddressNotAvailable => write!(f, "address not available"),
Self::PermissionDenied => write!(f, "permission denied"),
Self::ProtocolNotSupported => write!(f, "protocol not supported"),
Self::Io => write!(f, "I/O error"),
}
}
}
#[derive(Debug, Clone)]
pub struct TransportCapabilities {
pub ipv4: bool,
pub ipv6: bool,
pub relay: bool,
pub direct_udp: bool,
}
impl Default for TransportCapabilities {
fn default() -> Self {
Self {
ipv4: true,
ipv6: true,
relay: false,
direct_udp: true,
}
}
}
impl TransportCapabilities {
pub fn supports_address(&self, addr: &SocketAddr) -> bool {
match addr {
SocketAddr::V4(_) => self.ipv4,
SocketAddr::V6(_) => self.ipv6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FallbackStrategy {
NoRetry,
RetryOnce,
ExponentialBackoff {
max_retries: u32,
initial_delay_ms: u64,
},
#[default]
FallbackToRelay,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_result_success() {
let result: TransportResult<i32> = TransportResult::Success(42);
assert!(result.is_success());
assert!(!result.is_unsupported());
assert!(!result.should_retry());
assert_eq!(result.into_option(), Some(42));
}
#[test]
fn test_transport_result_unsupported() {
let result: TransportResult<i32> = TransportResult::Unsupported;
assert!(!result.is_success());
assert!(result.is_unsupported());
assert!(!result.should_retry());
assert_eq!(result.into_option(), None);
}
#[test]
fn test_transport_result_temporarily_unavailable() {
let result: TransportResult<i32> = TransportResult::TemporarilyUnavailable;
assert!(!result.is_success());
assert!(!result.is_unsupported());
assert!(result.should_retry());
}
#[test]
fn test_transport_result_into_result() {
let success: TransportResult<i32> = TransportResult::Success(42);
assert_eq!(success.into_result(), Ok(Some(42)));
let unsupported: TransportResult<i32> = TransportResult::Unsupported;
assert_eq!(unsupported.into_result(), Ok(None));
let error = TransportError::new(TransportErrorKind::Timeout, "test");
let failed: TransportResult<i32> = TransportResult::Failed(error.clone());
assert_eq!(failed.into_result(), Err(error));
}
#[test]
fn test_transport_error() {
let error = TransportError::connection_refused("127.0.0.1:5000".parse().unwrap());
assert_eq!(error.kind, TransportErrorKind::ConnectionRefused);
assert!(error.message.contains("127.0.0.1:5000"));
}
#[test]
fn test_transport_capabilities_default() {
let caps = TransportCapabilities::default();
assert!(caps.ipv4);
assert!(caps.ipv6);
assert!(!caps.relay);
assert!(caps.direct_udp);
}
#[test]
fn test_transport_capabilities_supports_address() {
let caps = TransportCapabilities {
ipv4: true,
ipv6: false,
relay: false,
direct_udp: true,
};
let v4_addr: SocketAddr = "127.0.0.1:5000".parse().unwrap();
let v6_addr: SocketAddr = "[::1]:5000".parse().unwrap();
assert!(caps.supports_address(&v4_addr));
assert!(!caps.supports_address(&v6_addr));
}
#[test]
fn test_fallback_strategy_default() {
let strategy = FallbackStrategy::default();
assert_eq!(strategy, FallbackStrategy::FallbackToRelay);
}
#[test]
fn test_transport_error_display() {
let error = TransportError::timeout("192.168.1.1:9000".parse().unwrap());
let display = format!("{}", error);
assert!(display.contains("timeout"));
assert!(display.contains("192.168.1.1:9000"));
}
}