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                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/// Errors that can occur when parsing or using endpoints.
116#[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}