Skip to main content

monocoque_core/
endpoint.rs

1//! Endpoint abstraction for transport-agnostic socket addressing.
2//!
3//! Provides unified addressing for TCP and IPC transports with parsing support.
4
5use std::fmt;
6use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::str::FromStr;
9
10/// Transport endpoint address.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum Endpoint {
13    /// TCP transport: `tcp://host:port`
14    Tcp(SocketAddr),
15    /// IPC transport (Unix domain socket): `ipc:///path/to/socket`
16    #[cfg(unix)]
17    Ipc(PathBuf),
18    /// In-process transport: `inproc://name`
19    Inproc(String),
20}
21
22impl Endpoint {
23    /// Parse an endpoint from a string.
24    ///
25    /// Supported formats:
26    /// - `tcp://127.0.0.1:5555`
27    /// - `tcp://[::1]:5555` (IPv6)
28    /// - `ipc:///tmp/socket.sock` (Unix only)
29    /// - `inproc://name`
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use monocoque_core::endpoint::Endpoint;
35    ///
36    /// let endpoint = Endpoint::parse("tcp://127.0.0.1:5555").unwrap();
37    /// assert!(matches!(endpoint, Endpoint::Tcp(_)));
38    ///
39    /// # #[cfg(unix)]
40    /// # {
41    /// let endpoint = Endpoint::parse("ipc:///tmp/test.sock").unwrap();
42    /// assert!(matches!(endpoint, Endpoint::Ipc(_)));
43    /// # }
44    ///
45    /// let endpoint = Endpoint::parse("inproc://my-endpoint").unwrap();
46    /// assert!(matches!(endpoint, Endpoint::Inproc(_)));
47    /// ```
48    pub fn parse(s: &str) -> Result<Self, EndpointError> {
49        s.parse()
50    }
51
52    /// Returns true if this is a TCP endpoint.
53    #[must_use]
54    pub const fn is_tcp(&self) -> bool {
55        matches!(self, Self::Tcp(_))
56    }
57
58    /// Returns true if this is an IPC endpoint.
59    #[cfg(unix)]
60    #[must_use]
61    pub const fn is_ipc(&self) -> bool {
62        matches!(self, Self::Ipc(_))
63    }
64
65    /// Returns true if this is an inproc endpoint.
66    #[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                if !path.starts_with('/') {
85                    return Err(EndpointError::InvalidScheme(s.to_string()));
86                }
87                Ok(Self::Ipc(PathBuf::from(path)))
88            }
89            #[cfg(not(unix))]
90            {
91                Err(EndpointError::IpcNotSupported)
92            }
93        } else if let Some(name) = s.strip_prefix("inproc://") {
94            if name.is_empty() {
95                Err(EndpointError::InvalidInprocName(
96                    "inproc name cannot be empty".to_string(),
97                ))
98            } else {
99                Ok(Self::Inproc(name.to_string()))
100            }
101        } else {
102            Err(EndpointError::InvalidScheme(s.to_string()))
103        }
104    }
105}
106
107impl fmt::Display for Endpoint {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match self {
110            Self::Tcp(addr) => write!(f, "tcp://{addr}"),
111            #[cfg(unix)]
112            Self::Ipc(path) => write!(f, "ipc://{}", path.display()),
113            Self::Inproc(name) => write!(f, "inproc://{name}"),
114        }
115    }
116}
117
118/// Errors that can occur when parsing or using endpoints.
119#[derive(Debug, thiserror::Error)]
120pub enum EndpointError {
121    #[error("Invalid scheme in endpoint: {0} (expected tcp://, ipc://, or inproc://)")]
122    InvalidScheme(String),
123
124    #[error("Invalid TCP address: {0}")]
125    InvalidTcpAddress(String),
126
127    #[error("Invalid inproc name: {0}")]
128    InvalidInprocName(String),
129
130    #[error("IPC transport not supported on this platform")]
131    IpcNotSupported,
132
133    #[error("I/O error: {0}")]
134    Io(#[from] std::io::Error),
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_parse_tcp_ipv4() {
143        let endpoint = Endpoint::parse("tcp://127.0.0.1:5555").unwrap();
144        assert!(matches!(endpoint, Endpoint::Tcp(_)));
145        assert_eq!(endpoint.to_string(), "tcp://127.0.0.1:5555");
146    }
147
148    #[test]
149    fn test_parse_tcp_ipv6() {
150        let endpoint = Endpoint::parse("tcp://[::1]:5555").unwrap();
151        assert!(matches!(endpoint, Endpoint::Tcp(_)));
152    }
153
154    #[cfg(unix)]
155    #[test]
156    fn test_parse_ipc() {
157        let endpoint = Endpoint::parse("ipc:///tmp/test.sock").unwrap();
158        assert!(matches!(endpoint, Endpoint::Ipc(_)));
159        assert_eq!(endpoint.to_string(), "ipc:///tmp/test.sock");
160    }
161
162    #[cfg(unix)]
163    #[test]
164    fn test_invalid_ipc_relative_path() {
165        let result = Endpoint::parse("ipc://relative.sock");
166        assert!(matches!(result, Err(EndpointError::InvalidScheme(_))));
167    }
168
169    #[test]
170    fn test_invalid_scheme() {
171        let result = Endpoint::parse("http://127.0.0.1:5555");
172        assert!(matches!(result, Err(EndpointError::InvalidScheme(_))));
173    }
174
175    #[test]
176    fn test_invalid_tcp_address() {
177        let result = Endpoint::parse("tcp://invalid:port");
178        assert!(matches!(result, Err(EndpointError::InvalidTcpAddress(_))));
179    }
180
181    #[test]
182    fn test_parse_inproc() {
183        let endpoint = Endpoint::parse("inproc://my-endpoint").unwrap();
184        assert!(matches!(endpoint, Endpoint::Inproc(_)));
185        assert_eq!(endpoint.to_string(), "inproc://my-endpoint");
186    }
187
188    #[test]
189    fn test_invalid_inproc_empty() {
190        let result = Endpoint::parse("inproc://");
191        assert!(matches!(result, Err(EndpointError::InvalidInprocName(_))));
192    }
193}