Skip to main content

mq_bridge/endpoints/memory/
transport.rs

1//  mq-bridge
2//  © Copyright 2025, by Marco Mengelkoch
3//  Licensed under MIT License, see License file for more details
4//  git clone https://github.com/marcomq/mq-bridge
5
6use crate::CanonicalMessage;
7use anyhow::{anyhow, Result};
8use async_trait::async_trait;
9
10/// Transport URL scheme and path information
11#[derive(Debug, Clone, PartialEq)]
12pub enum TransportUrl {
13    /// In-process memory transport: memory://namespace
14    Memory { namespace: String },
15    /// Unix Domain Socket: unix:///path or ipc://name or ipc:///path
16    #[cfg(unix)]
17    Unix { path: String },
18    /// Windows Named Pipe: pipe://name or ipc://name
19    #[cfg(windows)]
20    Pipe { name: String },
21}
22
23impl TransportUrl {
24    /// Parse a transport URL string
25    pub fn parse(url: &str) -> Result<Self> {
26        if url.is_empty() {
27            return Err(anyhow!("Transport URL cannot be empty"));
28        }
29
30        // Check for scheme
31        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                    // Platform-specific IPC handling
57                    #[cfg(unix)]
58                    {
59                        if rest.starts_with('/') {
60                            // Explicit path: ipc:///absolute/path
61                            Ok(TransportUrl::Unix {
62                                path: rest.to_string(),
63                            })
64                        } else {
65                            // Named socket: ipc://name -> /run/mq-bridge/name.sock
66                            let path = Self::default_unix_socket_path(rest)?;
67                            Ok(TransportUrl::Unix { path })
68                        }
69                    }
70                    #[cfg(windows)]
71                    {
72                        // Named pipe: ipc://name -> \\.\pipe\mq-bridge-name
73                        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            // No scheme, treat as memory namespace for backward compatibility
86            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        // Try /run/mq-bridge first (systemd standard)
95        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        // Try XDG_RUNTIME_DIR
101        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        // Fallback to /tmp (less secure)
109        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    /// Get a display name for logging
115    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    /// Get the namespace/identifier for channel lookup
126    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/// Abstract transport channel for sending/receiving message batches
138#[async_trait]
139pub trait TransportChannel: Send + Sync {
140    /// Send a batch of messages
141    async fn send_batch(&self, messages: Vec<CanonicalMessage>) -> Result<()>;
142
143    /// Receive a batch of messages (blocking)
144    async fn recv_batch(&self) -> Result<Vec<CanonicalMessage>>;
145
146    /// Try to receive a batch without blocking
147    fn try_recv_batch(&self) -> Result<Option<Vec<CanonicalMessage>>>;
148
149    /// Get the current number of batches in the channel
150    fn len(&self) -> usize;
151
152    /// Check if the channel is empty
153    fn is_empty(&self) -> bool {
154        self.len() == 0
155    }
156
157    /// Get the capacity of the channel
158    fn capacity(&self) -> Option<usize>;
159
160    /// Check if the channel is closed
161    fn is_closed(&self) -> bool;
162
163    /// Close the channel
164    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// Made with Bob