Skip to main content

aimdb_core/
transport.rs

1//! Transport connector traits for protocol-agnostic publishing
2//!
3//! Provides a generic `Connector` trait that enables scheme-based routing
4//! to different transport protocols. Each connector manages a single connection
5//! to a specific endpoint (e.g., one MQTT broker).
6//!
7//! # Design Philosophy
8//!
9//! - **Scheme-based routing**: the URL scheme (e.g. `mqtt://`, `knx://`) determines which connector handles requests
10//! - **Single endpoint per connector**: Each connector connects to ONE broker/resource
11//! - **Multi-transport publishing**: Same data can be published to multiple protocols
12//! - **Protocol-agnostic core**: Core knows schemes and key/value options, never protocol semantics
13
14use alloc::{boxed::Box, string::String, vec::Vec};
15use core::future::Future;
16use core::pin::Pin;
17
18/// Protocol-agnostic connector configuration
19///
20/// Carries the route's key/value options to [`Connector::publish`]. Only the
21/// genuinely protocol-agnostic `timeout_ms` is a typed field; every
22/// protocol-specific knob (e.g. MQTT's `qos`/`retain`) travels in
23/// [`protocol_options`](ConnectorConfig::protocol_options) and is interpreted
24/// by the connector with its own defaults. Connector crates expose typed
25/// setters as extension traits over the link builders (e.g. the MQTT
26/// connector's `MqttLinkExt`).
27#[derive(Debug, Clone)]
28pub struct ConnectorConfig {
29    /// Optional timeout in milliseconds for the publish/operation, as
30    /// interpreted by the connector
31    pub timeout_ms: Option<u32>,
32
33    /// Protocol-specific options as key-value pairs
34    /// Allows custom configuration without polluting the base struct
35    pub protocol_options: Vec<(String, String)>,
36}
37
38impl Default for ConnectorConfig {
39    fn default() -> Self {
40        Self {
41            timeout_ms: Some(5000),
42            protocol_options: Vec::new(),
43        }
44    }
45}
46
47impl ConnectorConfig {
48    /// Build a config from a route's URL-query key/value pairs.
49    ///
50    /// This is the shared seam the data-plane `pump_sink` helper uses to thread
51    /// per-route configuration through to [`Connector::publish`] without changing
52    /// the `publish` signature.
53    ///
54    /// Only the protocol-agnostic `timeout_ms` is lifted into the typed field;
55    /// every other key is passed through verbatim in
56    /// [`protocol_options`](ConnectorConfig::protocol_options) for the
57    /// connector to interpret with its own defaults.
58    pub fn from_query(query: &[(String, String)]) -> ConnectorConfig {
59        let mut cfg = ConnectorConfig::default();
60        for (k, v) in query {
61            match k.as_str() {
62                "timeout_ms" => {
63                    if let Ok(n) = v.parse::<u32>() {
64                        cfg.timeout_ms = Some(n);
65                    }
66                }
67                _ => cfg.protocol_options.push((k.clone(), v.clone())),
68            }
69        }
70        cfg
71    }
72}
73
74/// Error that can occur during connector publishing
75///
76/// Uses an enum instead of String for better performance in `no_std` environments
77/// and to enable defmt logging support in Embassy.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum PublishError {
80    /// Failed to connect to endpoint
81    ConnectionFailed,
82    /// Message payload too large for buffer
83    MessageTooLarge,
84    /// Quality of Service level not supported
85    UnsupportedQoS,
86    /// Network or operation timeout occurred
87    Timeout,
88    /// Buffer full, cannot queue message
89    BufferFull,
90    /// Invalid destination (topic, segment, endpoint)
91    InvalidDestination,
92}
93
94#[cfg(feature = "defmt")]
95impl defmt::Format for PublishError {
96    fn format(&self, f: defmt::Formatter) {
97        match self {
98            Self::ConnectionFailed => defmt::write!(f, "ConnectionFailed"),
99            Self::MessageTooLarge => defmt::write!(f, "MessageTooLarge"),
100            Self::UnsupportedQoS => defmt::write!(f, "UnsupportedQoS"),
101            Self::Timeout => defmt::write!(f, "Timeout"),
102            Self::BufferFull => defmt::write!(f, "BufferFull"),
103            Self::InvalidDestination => defmt::write!(f, "InvalidDestination"),
104        }
105    }
106}
107
108#[cfg(feature = "std")]
109impl std::fmt::Display for PublishError {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Self::ConnectionFailed => write!(f, "Failed to connect to endpoint"),
113            Self::MessageTooLarge => write!(f, "Message payload too large"),
114            Self::UnsupportedQoS => write!(f, "QoS level not supported"),
115            Self::Timeout => write!(f, "Operation timeout"),
116            Self::BufferFull => write!(f, "Buffer full, cannot queue message"),
117            Self::InvalidDestination => write!(f, "Invalid destination"),
118        }
119    }
120}
121
122#[cfg(feature = "std")]
123impl std::error::Error for PublishError {}
124
125/// Generic transport connector trait for protocol-agnostic publishing
126///
127/// This trait enables multi-protocol publishing via scheme-based routing
128/// (e.g. `mqtt://topic` → MQTT broker, `knx://1/0/6` → KNX group address).
129///
130/// Each connector manages ONE connection/endpoint. For multiple brokers/endpoints,
131/// create multiple connectors and register them with different schemes.
132///
133/// # Example Implementation
134///
135/// Illustrative sketch (not compiled: the MQTT client types are fictional —
136/// see `aimdb-mqtt-connector` for a real implementation):
137///
138/// ```rust,ignore
139/// impl Connector for MqttConnector {
140///     fn publish(
141///         &self,
142///         destination: &str,  // "sensors/temperature"
143///         config: &ConnectorConfig,
144///         payload: &[u8],
145///     ) -> Pin<Box<dyn Future<Output = Result<(), PublishError>> + Send + '_>> {
146///         // Protocol knobs come from the route's key/value options,
147///         // with connector-chosen defaults.
148///         let qos = config
149///             .protocol_options
150///             .iter()
151///             .find(|(k, _)| k == "qos")
152///             .and_then(|(_, v)| v.parse::<u8>().ok())
153///             .unwrap_or(1);
154///         Box::pin(async move {
155///             self.client.publish(destination, qos, payload).await
156///                 .map_err(|_| PublishError::ConnectionFailed)
157///         })
158///     }
159/// }
160/// ```
161///
162/// # Thread Safety
163///
164/// Requires Send + Sync for Tokio compatibility. For Embassy (single-threaded),
165/// use `unsafe impl Send + Sync` with safety documentation.
166pub trait Connector: Send + Sync {
167    /// Publish data to a protocol-specific destination
168    ///
169    /// # Arguments
170    /// * `destination` - Protocol-specific path, no broker/host info
171    ///   (e.g. an MQTT topic like "sensors/temperature")
172    /// * `config` - Publishing configuration (timeout + protocol options)
173    /// * `payload` - Message payload as byte slice
174    ///
175    /// # Returns
176    /// `Ok(())` on success, `PublishError` on failure
177    fn publish(
178        &self,
179        destination: &str,
180        config: &ConnectorConfig,
181        payload: &[u8],
182    ) -> Pin<Box<dyn Future<Output = Result<(), PublishError>> + Send + '_>>;
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use alloc::sync::Arc;
189
190    // Mock connector for testing
191    struct MockConnector;
192
193    impl Connector for MockConnector {
194        fn publish(
195            &self,
196            _destination: &str,
197            _config: &ConnectorConfig,
198            _payload: &[u8],
199        ) -> Pin<Box<dyn Future<Output = Result<(), PublishError>> + Send + '_>> {
200            Box::pin(async move { Ok(()) })
201        }
202    }
203
204    #[test]
205    fn test_connector_trait() {
206        let connector = Arc::new(MockConnector);
207
208        // Verify the connector can be used as a trait object
209        let _trait_obj: Arc<dyn Connector> = connector;
210    }
211
212    #[test]
213    fn test_connector_config_default() {
214        let config = ConnectorConfig::default();
215        assert_eq!(config.timeout_ms, Some(5000));
216        assert_eq!(config.protocol_options.len(), 0);
217    }
218
219    #[test]
220    fn test_publish_error_copy() {
221        let err = PublishError::ConnectionFailed;
222        let err2 = err; // Should be Copy
223        assert_eq!(err, err2);
224    }
225}