mq_bridge/endpoints/memory/
transport.rs1use crate::CanonicalMessage;
7use anyhow::{anyhow, Result};
8use async_trait::async_trait;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum TransportUrl {
13 Memory { namespace: String },
15 #[cfg(unix)]
17 Unix { path: String },
18 #[cfg(windows)]
20 Pipe { name: String },
21}
22
23impl TransportUrl {
24 pub fn parse(url: &str) -> Result<Self> {
26 if url.is_empty() {
27 return Err(anyhow!("Transport URL cannot be empty"));
28 }
29
30 if let Some((scheme, rest)) = url.split_once("://") {
32 match scheme {
33 "memory" if rest.is_empty() => Err(anyhow!("Memory namespace cannot be empty")),
34 "memory" => Ok(TransportUrl::Memory {
35 namespace: rest.to_string(),
36 }),
37 #[cfg(unix)]
38 "unix" => {
39 if !rest.starts_with('/') {
40 return Err(anyhow!("Unix socket path must be absolute: {}", url));
41 }
42 Ok(TransportUrl::Unix {
43 path: rest.to_string(),
44 })
45 }
46 #[cfg(windows)]
47 "pipe" if rest.is_empty() => Err(anyhow!("Pipe name cannot be empty")),
48 #[cfg(windows)]
49 "pipe" => Ok(TransportUrl::Pipe {
50 name: rest.to_string(),
51 }),
52 "ipc" => {
53 if rest.is_empty() {
54 return Err(anyhow!("IPC transport identifier cannot be empty"));
55 }
56 #[cfg(unix)]
58 {
59 if rest.starts_with('/') {
60 Ok(TransportUrl::Unix {
62 path: rest.to_string(),
63 })
64 } else {
65 let path = Self::default_unix_socket_path(rest)?;
67 Ok(TransportUrl::Unix { path })
68 }
69 }
70 #[cfg(windows)]
71 {
72 Ok(TransportUrl::Pipe {
74 name: format!("mq-bridge-{}", rest),
75 })
76 }
77 #[cfg(not(any(unix, windows)))]
78 {
79 Err(anyhow!("IPC transport not supported on this platform"))
80 }
81 }
82 _ => Err(anyhow!("Unknown transport scheme: {}", scheme)),
83 }
84 } else {
85 Ok(TransportUrl::Memory {
87 namespace: url.to_string(),
88 })
89 }
90 }
91
92 #[cfg(unix)]
93 fn default_unix_socket_path(name: &str) -> Result<String> {
94 let run_dir = std::path::Path::new("/run/mq-bridge");
96 if run_dir.exists() || std::fs::create_dir_all(run_dir).is_ok() {
97 return Ok(format!("/run/mq-bridge/{}.sock", name));
98 }
99
100 if let Ok(xdg_dir) = std::env::var("XDG_RUNTIME_DIR") {
102 let mq_dir = std::path::Path::new(&xdg_dir).join("mq-bridge");
103 if mq_dir.exists() || std::fs::create_dir_all(&mq_dir).is_ok() {
104 return Ok(format!("{}/{}.sock", mq_dir.display(), name));
105 }
106 }
107
108 let tmp_dir = std::path::Path::new("/tmp/mq-bridge");
110 std::fs::create_dir_all(tmp_dir)?;
111 Ok(format!("/tmp/mq-bridge/{}.sock", name))
112 }
113
114 pub fn display_name(&self) -> String {
116 match self {
117 TransportUrl::Memory { namespace } => format!("memory://{}", namespace),
118 #[cfg(unix)]
119 TransportUrl::Unix { path } => format!("unix://{}", path),
120 #[cfg(windows)]
121 TransportUrl::Pipe { name } => format!("pipe://{}", name),
122 }
123 }
124
125 pub fn namespace(&self) -> String {
127 match self {
128 TransportUrl::Memory { namespace } => namespace.clone(),
129 #[cfg(unix)]
130 TransportUrl::Unix { path } => path.clone(),
131 #[cfg(windows)]
132 TransportUrl::Pipe { name } => name.clone(),
133 }
134 }
135}
136
137#[async_trait]
139pub trait TransportChannel: Send + Sync {
140 async fn send_batch(&self, messages: Vec<CanonicalMessage>) -> Result<()>;
142
143 async fn recv_batch(&self) -> Result<Vec<CanonicalMessage>>;
145
146 fn try_recv_batch(&self) -> Result<Option<Vec<CanonicalMessage>>>;
148
149 fn len(&self) -> usize;
151
152 fn is_empty(&self) -> bool {
154 self.len() == 0
155 }
156
157 fn capacity(&self) -> Option<usize>;
159
160 fn is_closed(&self) -> bool;
162
163 fn close(&self);
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_parse_memory_url() {
173 let url = TransportUrl::parse("memory://test-topic").unwrap();
174 assert_eq!(
175 url,
176 TransportUrl::Memory {
177 namespace: "test-topic".to_string()
178 }
179 );
180 }
181
182 #[test]
183 fn test_parse_legacy_topic() {
184 let url = TransportUrl::parse("test-topic").unwrap();
185 assert_eq!(
186 url,
187 TransportUrl::Memory {
188 namespace: "test-topic".to_string()
189 }
190 );
191 }
192
193 #[cfg(unix)]
194 #[test]
195 fn test_parse_unix_socket() {
196 let url = TransportUrl::parse("unix:///tmp/test.sock").unwrap();
197 assert_eq!(
198 url,
199 TransportUrl::Unix {
200 path: "/tmp/test.sock".to_string()
201 }
202 );
203 }
204
205 #[cfg(unix)]
206 #[test]
207 fn test_parse_ipc_named() {
208 let url = TransportUrl::parse("ipc://worker").unwrap();
209 match url {
210 TransportUrl::Unix { path } => {
211 assert!(path.contains("worker.sock"));
212 }
213 _ => panic!("Expected Unix transport"),
214 }
215 }
216
217 #[cfg(unix)]
218 #[test]
219 fn test_parse_ipc_absolute() {
220 let url = TransportUrl::parse("ipc:///var/run/test.sock").unwrap();
221 assert_eq!(
222 url,
223 TransportUrl::Unix {
224 path: "/var/run/test.sock".to_string()
225 }
226 );
227 }
228
229 #[cfg(windows)]
230 #[test]
231 fn test_parse_pipe() {
232 let url = TransportUrl::parse("pipe://worker").unwrap();
233 assert_eq!(
234 url,
235 TransportUrl::Pipe {
236 name: "worker".to_string()
237 }
238 );
239 }
240
241 #[cfg(windows)]
242 #[test]
243 fn test_parse_ipc_windows() {
244 let url = TransportUrl::parse("ipc://worker").unwrap();
245 assert_eq!(
246 url,
247 TransportUrl::Pipe {
248 name: "mq-bridge-worker".to_string()
249 }
250 );
251 }
252
253 #[test]
254 fn test_parse_empty_url() {
255 let result = TransportUrl::parse("");
256 assert!(result.is_err());
257 }
258
259 #[test]
260 fn test_parse_unknown_scheme() {
261 let result = TransportUrl::parse("http://test");
262 assert!(result.is_err());
263 }
264}
265
266