use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossRegionRoute {
pub from_region: String,
pub to_region: String,
pub event_types: Vec<String>,
pub nats_subject: Option<String>,
}
impl CrossRegionRoute {
pub fn new(from: &str, to: &str, event_types: Vec<String>) -> Self {
Self {
from_region: from.to_string(),
to_region: to.to_string(),
event_types,
nats_subject: None,
}
}
pub fn with_subject(mut self, subject: &str) -> Self {
self.nats_subject = Some(subject.to_string());
self
}
pub fn subject(&self, prefix: &str) -> String {
self.nats_subject
.clone()
.unwrap_or_else(|| format!("{}.route.{}.{}", prefix, self.from_region, self.to_region))
}
}
#[derive(Debug, Clone, Default)]
pub struct FederationRoutingTable {
routes_by_source: HashMap<String, Vec<CrossRegionRoute>>,
routes_by_dest: HashMap<String, Vec<CrossRegionRoute>>,
}
impl FederationRoutingTable {
pub fn new() -> Self {
Self::default()
}
pub fn add_route(&mut self, route: CrossRegionRoute) {
self.routes_by_source
.entry(route.from_region.clone())
.or_default()
.push(route.clone());
self.routes_by_dest
.entry(route.to_region.clone())
.or_default()
.push(route);
}
pub fn remove_region(&mut self, region: &str) {
self.routes_by_source.remove(region);
self.routes_by_dest.remove(region);
for routes in self.routes_by_source.values_mut() {
routes.retain(|r| r.to_region != region);
}
for routes in self.routes_by_dest.values_mut() {
routes.retain(|r| r.from_region != region);
}
}
pub fn find_routes(&self, from_region: &str, event_type: &str) -> Vec<&CrossRegionRoute> {
self.routes_by_source
.get(from_region)
.map(|routes| {
routes
.iter()
.filter(|r| {
r.event_types
.iter()
.any(|pattern| event_type_matches(event_type, pattern))
})
.collect()
})
.unwrap_or_default()
}
pub fn routes_from(&self, region: &str) -> &[CrossRegionRoute] {
self.routes_by_source
.get(region)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn routes_to(&self, region: &str) -> &[CrossRegionRoute] {
self.routes_by_dest
.get(region)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn route_count(&self) -> usize {
self.routes_by_source.values().map(|v| v.len()).sum()
}
}
fn event_type_matches(event_type: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
event_type.starts_with(prefix)
} else {
event_type == pattern
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cross_region_route_subject() {
let route = CrossRegionRoute::new("us-east", "eu-west", vec!["Transaction".to_string()]);
assert_eq!(
route.subject("varpulis.federation"),
"varpulis.federation.route.us-east.eu-west"
);
let route = route.with_subject("custom.subject");
assert_eq!(route.subject("varpulis.federation"), "custom.subject");
}
#[test]
fn test_routing_table_add_and_find() {
let mut table = FederationRoutingTable::new();
table.add_route(CrossRegionRoute::new(
"us-east",
"eu-west",
vec!["Transaction".to_string(), "Alert*".to_string()],
));
table.add_route(CrossRegionRoute::new(
"us-east",
"ap-south",
vec!["*".to_string()],
));
let routes = table.find_routes("us-east", "Transaction");
assert_eq!(routes.len(), 2);
let routes = table.find_routes("us-east", "AlertFired");
assert_eq!(routes.len(), 2);
let routes = table.find_routes("unknown", "Transaction");
assert!(routes.is_empty());
}
#[test]
fn test_routing_table_remove_region() {
let mut table = FederationRoutingTable::new();
table.add_route(CrossRegionRoute::new(
"us-east",
"eu-west",
vec!["Transaction".to_string()],
));
table.add_route(CrossRegionRoute::new(
"eu-west",
"us-east",
vec!["Alert".to_string()],
));
assert_eq!(table.route_count(), 2);
table.remove_region("eu-west");
assert!(table.find_routes("us-east", "Transaction").is_empty());
assert!(table.routes_from("eu-west").is_empty());
}
#[test]
fn test_event_type_matches() {
assert!(event_type_matches("Transaction", "Transaction"));
assert!(event_type_matches("Transaction", "*"));
assert!(event_type_matches("TransactionCreated", "Transaction*"));
assert!(!event_type_matches("Alert", "Transaction"));
assert!(!event_type_matches("Alert", "Transaction*"));
}
}