use std::fmt;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Endpoint {
Tcp(SocketAddr),
#[cfg(unix)]
Ipc(PathBuf),
Inproc(String),
}
impl Endpoint {
pub fn parse(s: &str) -> Result<Self, EndpointError> {
s.parse()
}
#[must_use]
pub const fn is_tcp(&self) -> bool {
matches!(self, Self::Tcp(_))
}
#[cfg(unix)]
#[must_use]
pub const fn is_ipc(&self) -> bool {
matches!(self, Self::Ipc(_))
}
#[must_use]
pub const fn is_inproc(&self) -> bool {
matches!(self, Self::Inproc(_))
}
}
impl FromStr for Endpoint {
type Err = EndpointError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(addr) = s.strip_prefix("tcp://") {
let socket_addr = addr
.parse::<SocketAddr>()
.map_err(|_| EndpointError::InvalidTcpAddress(addr.to_string()))?;
Ok(Self::Tcp(socket_addr))
} else if let Some(path) = s.strip_prefix("ipc://") {
#[cfg(unix)]
{
Ok(Self::Ipc(PathBuf::from(path)))
}
#[cfg(not(unix))]
{
Err(EndpointError::IpcNotSupported)
}
} else if let Some(name) = s.strip_prefix("inproc://") {
if name.is_empty() {
Err(EndpointError::InvalidInprocName(
"inproc name cannot be empty".to_string(),
))
} else {
Ok(Self::Inproc(name.to_string()))
}
} else {
Err(EndpointError::InvalidScheme(s.to_string()))
}
}
}
impl fmt::Display for Endpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Tcp(addr) => write!(f, "tcp://{addr}"),
#[cfg(unix)]
Self::Ipc(path) => write!(f, "ipc://{}", path.display()),
Self::Inproc(name) => write!(f, "inproc://{name}"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum EndpointError {
#[error("Invalid scheme in endpoint: {0} (expected tcp://, ipc://, or inproc://)")]
InvalidScheme(String),
#[error("Invalid TCP address: {0}")]
InvalidTcpAddress(String),
#[error("Invalid inproc name: {0}")]
InvalidInprocName(String),
#[error("IPC transport not supported on this platform")]
IpcNotSupported,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_tcp_ipv4() {
let endpoint = Endpoint::parse("tcp://127.0.0.1:5555").unwrap();
assert!(matches!(endpoint, Endpoint::Tcp(_)));
assert_eq!(endpoint.to_string(), "tcp://127.0.0.1:5555");
}
#[test]
fn test_parse_tcp_ipv6() {
let endpoint = Endpoint::parse("tcp://[::1]:5555").unwrap();
assert!(matches!(endpoint, Endpoint::Tcp(_)));
}
#[cfg(unix)]
#[test]
fn test_parse_ipc() {
let endpoint = Endpoint::parse("ipc:///tmp/test.sock").unwrap();
assert!(matches!(endpoint, Endpoint::Ipc(_)));
assert_eq!(endpoint.to_string(), "ipc:///tmp/test.sock");
}
#[test]
fn test_invalid_scheme() {
let result = Endpoint::parse("http://127.0.0.1:5555");
assert!(matches!(result, Err(EndpointError::InvalidScheme(_))));
}
#[test]
fn test_invalid_tcp_address() {
let result = Endpoint::parse("tcp://invalid:port");
assert!(matches!(result, Err(EndpointError::InvalidTcpAddress(_))));
}
#[test]
fn test_parse_inproc() {
let endpoint = Endpoint::parse("inproc://my-endpoint").unwrap();
assert!(matches!(endpoint, Endpoint::Inproc(_)));
assert_eq!(endpoint.to_string(), "inproc://my-endpoint");
}
#[test]
fn test_invalid_inproc_empty() {
let result = Endpoint::parse("inproc://");
assert!(matches!(result, Err(EndpointError::InvalidInprocName(_))));
}
}