use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use super::bus::Group;
use super::subject::{SubjectPattern, check_publish};
#[derive(Debug)]
pub(crate) struct Route {
name: String,
filter: SubjectPattern,
group: Option<Group>,
stream: Option<String>,
}
impl Route {
pub(crate) fn core(name: &str, filter: SubjectPattern, queue: Option<&str>) -> Self {
Self {
name: name.to_owned(),
filter,
group: queue.map(|queue| Group::Queue(queue.to_owned())),
stream: None,
}
}
pub(crate) fn jetstream(
name: &str,
filter: SubjectPattern,
stream: &str,
durable: Option<&str>,
) -> Self {
Self {
name: name.to_owned(),
filter,
group: durable.map(|durable| Group::Durable {
stream: stream.to_owned(),
name: durable.to_owned(),
}),
stream: Some(stream.to_owned()),
}
}
}
#[derive(Debug)]
pub(crate) struct RouteBook {
no_echo: bool,
routes: Mutex<Vec<Route>>,
}
impl RouteBook {
pub(crate) const fn new(no_echo: bool) -> Self {
Self {
no_echo,
routes: Mutex::new(Vec::new()),
}
}
pub(crate) fn record(&self, route: Route) {
self.routes
.lock()
.expect("route book mutex poisoned")
.push(route);
}
pub(crate) fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
if check_publish(destination).is_err() {
return Vec::new();
}
let routes = self.routes.lock().expect("route book mutex poisoned");
let stored_in = routes
.iter()
.find(|route| route.stream.is_some() && route.filter.matches(destination))
.and_then(|route| route.stream.as_deref());
let mut seen: HashMap<&str, usize> = HashMap::new();
let mut groups: HashSet<&Group> = HashSet::new();
let mut reached = Vec::new();
for (position, name) in subscriptions.iter().enumerate() {
let occurrence = seen.entry(name).or_insert(0);
let route = routes
.iter()
.filter(|route| route.name == *name)
.nth(*occurrence);
*occurrence += 1;
let Some(route) = route else {
if SubjectPattern::parse(name).is_ok_and(|filter| filter.matches(destination)) {
reached.push(position);
}
continue;
};
let delivers = route
.stream
.as_ref()
.map_or(!self.no_echo, |stream| stored_in == Some(stream.as_str()));
if !delivers || !route.filter.matches(destination) {
continue;
}
if let Some(group) = &route.group
&& !groups.insert(group)
{
continue;
}
reached.push(position);
}
drop(routes);
reached
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pattern(subject: &str) -> SubjectPattern {
SubjectPattern::parse(subject).expect("pattern parses")
}
fn book(routes: Vec<Route>) -> RouteBook {
let book = RouteBook::new(false);
for route in routes {
book.record(route);
}
book
}
#[test]
fn every_matching_subscription_receives_a_publish() {
let book = book(vec![
Route::core("orders.created", pattern("orders.created"), None),
Route::core("orders.*", pattern("orders.*"), None),
Route::core("orders.>", pattern("orders.>"), None),
Route::core("payments", pattern("payments"), None),
]);
let names = ["orders.created", "orders.*", "orders.>", "payments"];
assert_eq!(book.routes("orders.created", &names), [0, 1, 2]);
assert_eq!(book.routes("orders.eu.created", &names), [2]);
assert_eq!(book.routes("payments", &names), [3]);
}
#[test]
fn a_queue_group_delivers_to_one_member_across_subjects() {
let book = book(vec![
Route::core("orders.*", pattern("orders.*"), Some("workers")),
Route::core("orders.>", pattern("orders.>"), Some("workers")),
Route::core("orders.*", pattern("orders.*"), Some("audit")),
]);
let names = ["orders.*", "orders.>", "orders.*"];
assert_eq!(book.routes("orders.created", &names), [0, 2]);
assert_eq!(book.routes("orders.eu.created", &names), [1]);
}
#[test]
fn a_consumer_receives_what_its_stream_stores_under_its_filter() {
let book = book(vec![
Route::jetstream(
"orders",
pattern("orders.created"),
"ORDERS",
Some("worker"),
),
Route::jetstream(
"orders",
pattern("orders.created"),
"ORDERS",
Some("worker"),
),
Route::jetstream("audit", pattern("orders.>"), "ORDERS", None),
]);
let names = ["orders", "orders", "audit"];
assert_eq!(book.routes("orders.created", &names), [0, 2]);
assert_eq!(book.routes("orders.cancelled", &names), [2]);
assert_eq!(book.routes("payments", &names), Vec::<usize>::new());
}
#[test]
fn a_connection_without_echo_reaches_no_core_subscription_of_its_own() {
let book = RouteBook::new(true);
book.record(Route::core("orders", pattern("orders"), None));
book.record(Route::jetstream(
"stored",
pattern("orders"),
"ORDERS",
None,
));
assert_eq!(book.routes("orders", &["orders", "stored"]), [1]);
}
}