use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
use proxima_core::live::{Live, LiveControl, live};
use serde::{Deserialize, Serialize};
use crate::pipe::live_filter::{FilterControl, IdSet, LiveFilter, live_filter_ids};
struct Subscription<Id: Ord> {
filter: LiveFilter<IdSet<Id>>,
control: FilterControl<IdSet<Id>>,
}
impl<Id: Ord> Clone for Subscription<Id> {
fn clone(&self) -> Self {
Self {
filter: self.filter.clone(),
control: self.control.clone(),
}
}
}
type Subscriptions<Id> = BTreeMap<String, Subscription<Id>>;
pub struct FilterRegistry<Id: Ord> {
read: Live<Subscriptions<Id>>,
write: LiveControl<Subscriptions<Id>>,
}
impl<Id: Ord + Clone> FilterRegistry<Id> {
#[must_use]
pub fn new() -> Self {
let (read, write) = live(BTreeMap::new());
Self { read, write }
}
pub fn subscribe(
&self,
name: impl Into<String>,
ids: impl IntoIterator<Item = Id>,
) -> (LiveFilter<IdSet<Id>>, FilterControl<IdSet<Id>>) {
let name = name.into();
let (filter, control) = live_filter_ids(ids);
let entry = Subscription {
filter: filter.clone(),
control: control.clone(),
};
self.write.update(|current| {
let mut next = current.clone();
next.insert(name.clone(), entry.clone());
next
});
(filter, control)
}
pub fn unsubscribe(&self, name: &str) -> bool {
let existed = self.read.read(|subs| subs.contains_key(name));
if existed {
self.write.update(|current| {
let mut next = current.clone();
next.remove(name);
next
});
}
existed
}
#[must_use]
pub fn filter(&self, name: &str) -> Option<LiveFilter<IdSet<Id>>> {
self.read
.read(|subs| subs.get(name).map(|entry| entry.filter.clone()))
}
#[must_use]
pub fn control(&self, name: &str) -> Option<FilterControl<IdSet<Id>>> {
self.read
.read(|subs| subs.get(name).map(|entry| entry.control.clone()))
}
#[must_use]
pub fn names(&self) -> Vec<String> {
self.read.read(|subs| subs.keys().cloned().collect())
}
#[must_use]
pub fn matches(&self, id: &Id) -> Vec<String> {
self.read.read(|subs| {
subs.iter()
.filter(|(_, entry)| entry.filter.contains(id))
.map(|(name, _)| name.clone())
.collect()
})
}
#[must_use]
pub fn from_config(config: &FilterRegistryConfig<Id>) -> Self {
let registry = Self::new();
for (name, ids) in &config.subscriptions {
registry.subscribe(name.clone(), ids.iter().cloned());
}
registry
}
#[must_use]
pub fn to_config(&self) -> FilterRegistryConfig<Id> {
self.read.read(|subs| {
let subscriptions = subs
.iter()
.map(|(name, entry)| {
let ids = entry.filter.snapshot().iter().cloned().collect();
(name.clone(), ids)
})
.collect();
FilterRegistryConfig { subscriptions }
})
}
}
impl<Id: Ord + Clone> Default for FilterRegistry<Id> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FilterRegistryConfig<Id: Ord> {
subscriptions: BTreeMap<String, BTreeSet<Id>>,
}
impl<Id: Ord> FilterRegistryConfig<Id> {
#[must_use]
pub fn new() -> Self {
Self {
subscriptions: BTreeMap::new(),
}
}
#[must_use]
pub fn subscribe(mut self, name: impl Into<String>, ids: impl IntoIterator<Item = Id>) -> Self {
self.subscriptions
.insert(name.into(), ids.into_iter().collect());
self
}
}
#[cfg(feature = "config")]
impl<Id: Ord> conflaguration::Validate for FilterRegistryConfig<Id> {
fn validate(&self) -> conflaguration::Result<()> {
let errors: Vec<conflaguration::ValidationMessage> = self
.subscriptions
.keys()
.filter(|name| name.trim().is_empty())
.map(|_| {
conflaguration::ValidationMessage::new(
"name",
"subscription name must be non-empty",
)
})
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(conflaguration::Error::Validation { errors })
}
}
}
#[cfg(feature = "config")]
impl<Id: Ord + Serialize + serde::de::DeserializeOwned> FilterRegistryConfig<Id> {
pub fn load(path: impl AsRef<std::path::Path>) -> Result<Self, proxima_core::ProximaError> {
conflaguration::ConfigBuilder::<Self>::new()
.file(path)
.validate()
.build()
.map_err(|err| {
proxima_core::ProximaError::Registry(format!(
"filter registry config load failed: {err}"
))
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn ids() -> (String, String, String) {
(
"req-7f3a9c2e1b4d".to_string(),
"req-0f43d9c0aa18".to_string(),
"req-c06db8bb5521".to_string(),
)
}
#[test]
fn subscribe_then_look_up_by_name() {
let (first, other, _) = ids();
let registry: FilterRegistry<String> = FilterRegistry::new();
registry.subscribe("orders", [first.clone()]);
let filter = registry.filter("orders").expect("subscription exists");
assert!(filter.contains(&first));
assert!(!filter.contains(&other));
assert!(registry.filter("absent").is_none());
}
#[test]
fn control_by_name_reconfigures_the_named_subscription() {
let (first, second, _) = ids();
let registry: FilterRegistry<String> = FilterRegistry::new();
let (filter, _control) = registry.subscribe("orders", [first.clone()]);
registry
.control("orders")
.expect("control")
.add(second.clone());
assert!(
filter.contains(&second),
"the data-path filter sees the control-plane add"
);
}
#[test]
fn unsubscribe_removes_a_subscription() {
let (first, _, _) = ids();
let registry: FilterRegistry<String> = FilterRegistry::new();
registry.subscribe("orders", [first]);
assert!(registry.unsubscribe("orders"));
assert!(
!registry.unsubscribe("orders"),
"second removal reports absent"
);
assert!(registry.filter("orders").is_none());
}
#[test]
fn names_lists_every_subscription_sorted() {
let registry: FilterRegistry<String> = FilterRegistry::new();
registry.subscribe("beta", ["x".to_string()]);
registry.subscribe("alpha", ["y".to_string()]);
assert_eq!(
registry.names(),
vec!["alpha".to_string(), "beta".to_string()]
);
}
#[test]
fn matches_returns_the_subscribers_for_an_id() {
let (shared, only_a, _) = ids();
let registry: FilterRegistry<String> = FilterRegistry::new();
registry.subscribe("a", [shared.clone(), only_a.clone()]);
registry.subscribe("b", [shared.clone()]);
assert_eq!(
registry.matches(&shared),
vec!["a".to_string(), "b".to_string()]
);
assert_eq!(registry.matches(&only_a), vec!["a".to_string()]);
}
#[test]
fn from_config_builds_and_to_config_mirrors() {
let (first, second, third) = ids();
let config = FilterRegistryConfig::new()
.subscribe("orders", [first.clone(), second.clone()])
.subscribe("audit", [third.clone()]);
let registry = FilterRegistry::from_config(&config);
assert!(registry.filter("orders").expect("orders").contains(&first));
assert!(registry.filter("audit").expect("audit").contains(&third));
assert_eq!(
registry.to_config(),
config,
"built registry mirrors back to its config"
);
}
#[test]
fn to_config_reflects_live_mutations() {
let (first, second, _) = ids();
let registry: FilterRegistry<String> = FilterRegistry::new();
registry.subscribe("orders", [first.clone()]);
registry
.control("orders")
.expect("control")
.add(second.clone());
let mirrored = registry.to_config();
let expected = FilterRegistryConfig::new().subscribe("orders", [first, second]);
assert_eq!(mirrored, expected, "the mirror captures the live add");
}
#[test]
fn config_round_trips_through_serde() {
let (first, second, _) = ids();
let config = FilterRegistryConfig::new().subscribe("orders", [first, second]);
let serialized = serde_json::to_string(&config).unwrap();
assert!(
serialized.starts_with('{'),
"a registry config is a name->ids map"
);
let restored: FilterRegistryConfig<String> = serde_json::from_str(&serialized).unwrap();
assert_eq!(restored, config);
}
#[cfg(feature = "config")]
#[test]
fn conflaguration_validates_a_seeded_config() {
use conflaguration::ConfigBuilder;
let (first, _, _) = ids();
let config = FilterRegistryConfig::new().subscribe("orders", [first]);
let built = ConfigBuilder::<FilterRegistryConfig<String>>::new()
.value(config.clone())
.validate()
.build()
.expect("validated build");
assert_eq!(
built, config,
"value-seeded + validated build returns the config"
);
}
#[cfg(feature = "config")]
#[test]
fn conflaguration_rejects_a_blank_name() {
use conflaguration::Validate;
let (first, _, _) = ids();
let config = FilterRegistryConfig::new().subscribe(" ", [first]);
assert!(
config.validate().is_err(),
"a blank subscription name is rejected"
);
}
}