monocoque_core/
endpoint.rs1use std::fmt;
6use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::str::FromStr;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum Endpoint {
13 Tcp(SocketAddr),
15 #[cfg(unix)]
17 Ipc(PathBuf),
18 Inproc(String),
20}
21
22impl Endpoint {
23 pub fn parse(s: &str) -> Result<Self, EndpointError> {
49 s.parse()
50 }
51
52 #[must_use]
54 pub const fn is_tcp(&self) -> bool {
55 matches!(self, Self::Tcp(_))
56 }
57
58 #[cfg(unix)]
60 #[must_use]
61 pub const fn is_ipc(&self) -> bool {
62 matches!(self, Self::Ipc(_))
63 }
64
65 #[must_use]
67 pub const fn is_inproc(&self) -> bool {
68 matches!(self, Self::Inproc(_))
69 }
70}
71
72impl FromStr for Endpoint {
73 type Err = EndpointError;
74
75 fn from_str(s: &str) -> Result<Self, Self::Err> {
76 if let Some(addr) = s.strip_prefix("tcp://") {
77 let socket_addr = addr
78 .parse::<SocketAddr>()
79 .map_err(|_| EndpointError::InvalidTcpAddress(addr.to_string()))?;
80 Ok(Self::Tcp(socket_addr))
81 } else if let Some(path) = s.strip_prefix("ipc://") {
82 #[cfg(unix)]
83 {
84 Ok(Self::Ipc(PathBuf::from(path)))
85 }
86 #[cfg(not(unix))]
87 {
88 Err(EndpointError::IpcNotSupported)
89 }
90 } else if let Some(name) = s.strip_prefix("inproc://") {
91 if name.is_empty() {
92 Err(EndpointError::InvalidInprocName(
93 "inproc name cannot be empty".to_string(),
94 ))
95 } else {
96 Ok(Self::Inproc(name.to_string()))
97 }
98 } else {
99 Err(EndpointError::InvalidScheme(s.to_string()))
100 }
101 }
102}
103
104impl fmt::Display for Endpoint {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Tcp(addr) => write!(f, "tcp://{addr}"),
108 #[cfg(unix)]
109 Self::Ipc(path) => write!(f, "ipc://{}", path.display()),
110 Self::Inproc(name) => write!(f, "inproc://{name}"),
111 }
112 }
113}
114
115#[derive(Debug, thiserror::Error)]
117pub enum EndpointError {
118 #[error("Invalid scheme in endpoint: {0} (expected tcp://, ipc://, or inproc://)")]
119 InvalidScheme(String),
120
121 #[error("Invalid TCP address: {0}")]
122 InvalidTcpAddress(String),
123
124 #[error("Invalid inproc name: {0}")]
125 InvalidInprocName(String),
126
127 #[error("IPC transport not supported on this platform")]
128 IpcNotSupported,
129
130 #[error("I/O error: {0}")]
131 Io(#[from] std::io::Error),
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_parse_tcp_ipv4() {
140 let endpoint = Endpoint::parse("tcp://127.0.0.1:5555").unwrap();
141 assert!(matches!(endpoint, Endpoint::Tcp(_)));
142 assert_eq!(endpoint.to_string(), "tcp://127.0.0.1:5555");
143 }
144
145 #[test]
146 fn test_parse_tcp_ipv6() {
147 let endpoint = Endpoint::parse("tcp://[::1]:5555").unwrap();
148 assert!(matches!(endpoint, Endpoint::Tcp(_)));
149 }
150
151 #[cfg(unix)]
152 #[test]
153 fn test_parse_ipc() {
154 let endpoint = Endpoint::parse("ipc:///tmp/test.sock").unwrap();
155 assert!(matches!(endpoint, Endpoint::Ipc(_)));
156 assert_eq!(endpoint.to_string(), "ipc:///tmp/test.sock");
157 }
158
159 #[test]
160 fn test_invalid_scheme() {
161 let result = Endpoint::parse("http://127.0.0.1:5555");
162 assert!(matches!(result, Err(EndpointError::InvalidScheme(_))));
163 }
164
165 #[test]
166 fn test_invalid_tcp_address() {
167 let result = Endpoint::parse("tcp://invalid:port");
168 assert!(matches!(result, Err(EndpointError::InvalidTcpAddress(_))));
169 }
170
171 #[test]
172 fn test_parse_inproc() {
173 let endpoint = Endpoint::parse("inproc://my-endpoint").unwrap();
174 assert!(matches!(endpoint, Endpoint::Inproc(_)));
175 assert_eq!(endpoint.to_string(), "inproc://my-endpoint");
176 }
177
178 #[test]
179 fn test_invalid_inproc_empty() {
180 let result = Endpoint::parse("inproc://");
181 assert!(matches!(result, Err(EndpointError::InvalidInprocName(_))));
182 }
183}