use std::{
cmp::Ordering,
future::{Future, ready},
};
use thiserror::Error;
use tokio::sync::mpsc;
use super::{ConnectedMemoryBroker, Discarding, LogMode, MemoryError, MemorySubscriber, Sender};
use crate::{AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Routing {
#[default]
EveryMatch,
MostSpecific,
}
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PatternError {
#[error("token {token} is empty; tokens are separated by single dots")]
EmptyToken {
token: usize,
},
#[error("`>` at token {token} is not the last token; it matches the rest of a name")]
TailNotLast {
token: usize,
},
#[error("it has no `*` or `>` token; subscribe to one name with `MemorySource`")]
NoWildcard,
}
pub(super) fn has_wildcard(name: &str) -> bool {
name.split('.').any(|token| token == "*" || token == ">")
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Token {
Literal(Box<str>),
One,
Tail,
}
impl Token {
const fn rank(&self) -> u8 {
match self {
Self::Literal(_) => 2,
Self::One => 1,
Self::Tail => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Pattern {
tokens: Box<[Token]>,
}
impl Pattern {
pub(super) fn parse(pattern: &str) -> Result<Self, PatternError> {
let count = pattern.split('.').count();
let mut wildcard = false;
let tokens = pattern
.split('.')
.enumerate()
.map(|(index, token)| match token {
"" => Err(PatternError::EmptyToken { token: index }),
">" if index + 1 < count => Err(PatternError::TailNotLast { token: index }),
">" => {
wildcard = true;
Ok(Token::Tail)
}
"*" => {
wildcard = true;
Ok(Token::One)
}
literal => Ok(Token::Literal(literal.into())),
})
.collect::<Result<Box<[Token]>, PatternError>>()?;
if !wildcard {
return Err(PatternError::NoWildcard);
}
Ok(Self { tokens })
}
pub(super) fn matches(&self, name: &str) -> bool {
let mut parts = name.split('.');
for token in &*self.tokens {
match token {
Token::Tail => {
return parts.next().is_some_and(|part| !part.is_empty())
&& parts.all(|part| !part.is_empty());
}
Token::One => {
if parts.next().is_none_or(str::is_empty) {
return false;
}
}
Token::Literal(literal) => {
if parts.next() != Some(&**literal) {
return false;
}
}
}
}
parts.next().is_none()
}
pub(super) fn specificity(&self, other: &Self) -> Ordering {
self.tokens
.iter()
.map(Token::rank)
.cmp(other.tokens.iter().map(Token::rank))
}
}
pub(super) struct PatternGroup {
pub(super) text: Box<str>,
pub(super) pattern: Pattern,
pub(super) senders: Vec<Sender>,
}
impl PatternGroup {
pub(super) fn is_live(&self) -> bool {
self.senders.iter().any(|sender| !sender.is_closed())
}
}
pub(super) enum PatternReach {
Nobody,
EveryFrom(usize),
Only(usize),
}
impl Routing {
pub(super) fn reach(self, groups: &[PatternGroup], name: &str, exact: bool) -> PatternReach {
match self {
Self::EveryMatch => groups
.iter()
.position(|group| group.pattern.matches(name))
.map_or(PatternReach::Nobody, PatternReach::EveryFrom),
Self::MostSpecific if exact => PatternReach::Nobody,
Self::MostSpecific => groups
.iter()
.enumerate()
.filter(|(_, group)| group.pattern.matches(name) && group.is_live())
.max_by(|(_, left), (_, right)| left.pattern.specificity(&right.pattern))
.map_or(PatternReach::Nobody, |(index, _)| PatternReach::Only(index)),
}
}
pub(super) fn routes(self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
let exact: Vec<usize> = subscriptions
.iter()
.enumerate()
.filter(|(_, name)| !has_wildcard(name) && **name == destination)
.map(|(position, _)| position)
.collect();
let patterns: Vec<(usize, Pattern)> = subscriptions
.iter()
.enumerate()
.filter(|(_, name)| has_wildcard(name))
.filter_map(|(position, name)| Some((position, Pattern::parse(name).ok()?)))
.filter(|(_, pattern)| pattern.matches(destination))
.collect();
match self {
Self::EveryMatch => {
let mut every: Vec<usize> = exact
.into_iter()
.chain(patterns.into_iter().map(|(position, _)| position))
.collect();
every.sort_unstable();
every
}
Self::MostSpecific if !exact.is_empty() => exact,
Self::MostSpecific => {
let Some((_, best)) = patterns
.iter()
.max_by(|(_, left), (_, right)| left.specificity(right))
else {
return Vec::new();
};
patterns
.iter()
.filter(|(_, pattern)| pattern == best)
.map(|(position, _)| *position)
.collect()
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct MemoryPattern {
pattern: String,
}
impl MemoryPattern {
pub fn new(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
}
}
}
impl<Log: LogMode> SubscriptionSource<ConnectedMemoryBroker<Log>> for MemoryPattern {
type Subscriber = MemorySubscriber<Discarding>;
type Copies = AddressedCopies;
fn name(&self) -> &str {
&self.pattern
}
fn subscribe(
self,
connected: &ConnectedMemoryBroker<Log>,
) -> impl Future<Output = Result<Self::Subscriber, MemoryError>> + Send {
let subscribed = match Pattern::parse(&self.pattern) {
Ok(pattern) => {
let (tx, rx) = mpsc::unbounded_channel();
connected
.state
.register_pattern(&self.pattern, pattern, tx.clone())
.map(|()| {
MemorySubscriber::new(
self.pattern,
rx,
tx,
&connected.state,
Some(connected.runtime.clone()),
)
})
}
Err(reason) => Err(MemoryError::InvalidPattern {
pattern: self.pattern,
reason,
}),
};
ready(subscribed)
}
}
impl<Log: LogMode> RedeliveryAddressed<ConnectedMemoryBroker<Log>> for MemoryPattern {
fn redelivery_address(
&self,
_connected: &ConnectedMemoryBroker<Log>,
) -> impl Future<Output = Result<RedeliveryAddress, MemoryError>> + Send {
ready(Ok(RedeliveryAddress::new(self.pattern.clone())))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(pattern: &str) -> Pattern {
Pattern::parse(pattern).expect("a valid pattern")
}
#[test]
fn one_matches_exactly_one_token() {
let pattern = parse("orders.*");
assert!(pattern.matches("orders.eu"));
assert!(!pattern.matches("orders"));
assert!(!pattern.matches("orders.eu.created"));
assert!(!pattern.matches("invoices.eu"));
}
#[test]
fn tail_matches_one_or_more_tokens() {
let pattern = parse("orders.>");
assert!(pattern.matches("orders.eu"));
assert!(pattern.matches("orders.eu.created"));
assert!(!pattern.matches("orders"));
assert!(parse(">").matches("anything.at.all"));
}
#[test]
fn an_empty_token_matches_no_wildcard() {
assert!(!parse("orders.*").matches("orders."));
assert!(!parse("orders.*").matches(".eu"));
assert!(!parse("orders.>").matches("orders."));
assert!(!parse("orders.>").matches("orders..eu"));
assert!(!parse("orders.>").matches("orders.eu."));
assert!(!parse("*.eu").matches(".eu"));
assert!(parse("orders.>").matches("orders.eu.created"));
}
#[test]
fn faults_are_reported_from_the_left() {
assert_eq!(
Pattern::parse("orders..*"),
Err(PatternError::EmptyToken { token: 1 })
);
assert_eq!(
Pattern::parse("orders.>.eu"),
Err(PatternError::TailNotLast { token: 1 })
);
assert_eq!(
Pattern::parse("*."),
Err(PatternError::EmptyToken { token: 1 })
);
assert_eq!(Pattern::parse("orders.eu"), Err(PatternError::NoWildcard));
assert_eq!(Pattern::parse("orders.eu*"), Err(PatternError::NoWildcard));
assert!(!has_wildcard("orders.eu*"));
}
#[test]
fn a_literal_beats_one_and_one_beats_tail_from_the_left() {
let ordered = [
"orders.eu.*",
"orders.*.created",
"orders.>",
"*.eu.created",
];
for pair in ordered.windows(2) {
assert_eq!(
parse(pair[0]).specificity(&parse(pair[1])),
Ordering::Greater,
"{} is more specific than {}",
pair[0],
pair[1]
);
}
}
#[test]
fn routes_follow_the_rule() {
let subscriptions = ["orders.eu", "orders.*", "orders.>", "orders.*"];
assert_eq!(
Routing::EveryMatch.routes("orders.eu", &subscriptions),
[0, 1, 2, 3]
);
assert_eq!(
Routing::MostSpecific.routes("orders.eu", &subscriptions),
[0]
);
assert_eq!(
Routing::MostSpecific.routes("orders.us", &subscriptions),
[1, 3]
);
assert_eq!(
Routing::MostSpecific.routes("orders.us.created", &subscriptions),
[2]
);
assert!(
Routing::MostSpecific
.routes("invoices", &subscriptions)
.is_empty()
);
}
}