use alloc::{string::String, sync::Arc, vec::Vec};
use crate::connector::IngestFn;
pub struct Route {
pub resource_id: Arc<str>,
pub ingest: IngestFn,
}
pub struct Router {
routes: Vec<Route>,
}
impl Router {
pub fn new(routes: Vec<Route>) -> Self {
Self { routes }
}
pub fn route(
&self,
resource_id: &str,
payload: &[u8],
ctx: &crate::RuntimeContext,
) -> Result<(), String> {
let mut routed = false;
let mut matched = false;
for route in &self.routes {
if route.resource_id.as_ref() == resource_id {
matched = true;
match (route.ingest)(ctx, payload) {
Ok(()) => {
routed = true;
log_debug!("Routed message on '{}' to producer", resource_id);
}
Err(_e) => {
log_warn!("Failed to ingest message on '{}': {}", resource_id, _e);
#[cfg(feature = "defmt")]
defmt::warn!(
"Failed to ingest message on '{}': {}",
resource_id,
_e.as_str()
);
}
}
}
}
if !routed {
if matched {
log_debug!(
"Route matched for '{}' but message was not produced (ingest errors)",
resource_id
);
#[cfg(feature = "defmt")]
defmt::debug!("Route matched for '{}' but not produced", resource_id);
} else {
log_debug!("No route found for resource: '{}'", resource_id);
#[cfg(feature = "defmt")]
defmt::debug!("No route found for resource: '{}'", resource_id);
}
}
Ok(())
}
pub fn resource_ids(&self) -> Vec<Arc<str>> {
let mut ids: Vec<Arc<str>> = self.routes.iter().map(|r| r.resource_id.clone()).collect();
ids.sort_unstable_by(|a, b| a.as_ref().cmp(b.as_ref()));
ids.dedup_by(|a, b| a.as_ref() == b.as_ref());
ids
}
pub fn route_count(&self) -> usize {
self.routes.len()
}
}
pub struct RouterBuilder {
routes: Vec<Route>,
}
impl RouterBuilder {
pub fn new() -> Self {
Self { routes: Vec::new() }
}
pub fn from_routes(routes: Vec<(String, IngestFn)>) -> Self {
let mut builder = Self::new();
for (resource_id, ingest) in routes {
let resource_id_arc: Arc<str> = Arc::from(resource_id.as_str());
builder = builder.add_route(resource_id_arc, ingest);
}
builder
}
pub fn add_route(mut self, resource_id: Arc<str>, ingest: IngestFn) -> Self {
self.routes.push(Route {
resource_id,
ingest,
});
self
}
pub fn build(self) -> Router {
Router::new(self.routes)
}
pub fn route_count(&self) -> usize {
self.routes.len()
}
}
impl Default for RouterBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn test_ctx() -> crate::RuntimeContext {
crate::RuntimeContext::new(Arc::new(crate::executor::test_support::NoopRuntimeOps))
}
fn counting_ingest(call_count: Arc<AtomicUsize>) -> IngestFn {
Arc::new(move |_ctx, _payload| {
call_count.fetch_add(1, Ordering::SeqCst);
Ok(())
})
}
#[test]
fn test_single_route() {
let call_count = Arc::new(AtomicUsize::new(0));
let routes = vec![Route {
resource_id: Arc::from("test/resource"),
ingest: counting_ingest(call_count.clone()),
}];
let router = Router::new(routes);
router
.route("test/resource", b"dummy", &test_ctx())
.unwrap();
assert_eq!(call_count.load(Ordering::SeqCst), 1);
}
#[test]
fn test_multiple_routes_same_resource() {
let call_count1 = Arc::new(AtomicUsize::new(0));
let call_count2 = Arc::new(AtomicUsize::new(0));
let routes = vec![
Route {
resource_id: Arc::from("shared/resource"),
ingest: counting_ingest(call_count1.clone()),
},
Route {
resource_id: Arc::from("shared/resource"),
ingest: counting_ingest(call_count2.clone()),
},
];
let router = Router::new(routes);
router
.route("shared/resource", b"dummy", &test_ctx())
.unwrap();
assert_eq!(call_count1.load(Ordering::SeqCst), 1);
assert_eq!(call_count2.load(Ordering::SeqCst), 1);
}
#[test]
fn test_unknown_resource() {
let call_count = Arc::new(AtomicUsize::new(0));
let routes = vec![Route {
resource_id: Arc::from("test/resource"),
ingest: counting_ingest(call_count.clone()),
}];
let router = Router::new(routes);
router
.route("unknown/resource", b"dummy", &test_ctx())
.unwrap();
assert_eq!(call_count.load(Ordering::SeqCst), 0);
}
#[test]
fn test_resource_ids_deduplication() {
let routes = vec![
Route {
resource_id: Arc::from("resource1"),
ingest: counting_ingest(Arc::new(AtomicUsize::new(0))),
},
Route {
resource_id: Arc::from("resource1"), ingest: counting_ingest(Arc::new(AtomicUsize::new(0))),
},
Route {
resource_id: Arc::from("resource2"),
ingest: counting_ingest(Arc::new(AtomicUsize::new(0))),
},
];
let router = Router::new(routes);
let ids = router.resource_ids();
assert_eq!(ids.len(), 2);
assert!(ids.iter().any(|id| id.as_ref() == "resource1"));
assert!(ids.iter().any(|id| id.as_ref() == "resource2"));
}
#[test]
fn test_ingest_receives_payload_and_ctx() {
let seen_len = Arc::new(AtomicUsize::new(0));
let seen_len_clone = seen_len.clone();
let ingest: IngestFn = Arc::new(move |_ctx, payload| {
seen_len_clone.store(payload.len(), Ordering::SeqCst);
Ok(())
});
let routes = vec![Route {
resource_id: Arc::from("ctx/resource"),
ingest,
}];
let router = Router::new(routes);
router.route("ctx/resource", b"dummy", &test_ctx()).unwrap();
assert_eq!(seen_len.load(Ordering::SeqCst), 5);
}
#[test]
fn test_ingest_error_does_not_propagate() {
let ingest: IngestFn = Arc::new(|_ctx, _payload| Err("deserialize failed".into()));
let routes = vec![Route {
resource_id: Arc::from("err/resource"),
ingest,
}];
let router = Router::new(routes);
router.route("err/resource", b"dummy", &test_ctx()).unwrap();
}
}