use alloc::collections::BTreeSet;
use alloc::sync::Arc;
use core::future::Future;
use proxima_core::live::{Live, LiveControl, live};
use serde::{Deserialize, Serialize};
use crate::pipe::SendPipe;
pub struct LiveFilter<Predicate> {
inner: Live<Predicate>,
}
pub struct FilterControl<Predicate> {
control: LiveControl<Predicate>,
}
#[must_use]
pub fn live_filter<Predicate>(
initial: Predicate,
) -> (LiveFilter<Predicate>, FilterControl<Predicate>) {
let (inner, control) = live(initial);
(LiveFilter { inner }, FilterControl { control })
}
#[must_use]
pub fn live_filter_ids<Id: Ord + Clone>(
ids: impl IntoIterator<Item = Id>,
) -> (LiveFilter<IdSet<Id>>, FilterControl<IdSet<Id>>) {
live_filter(IdSet::from_ids(ids))
}
impl<Predicate> Clone for LiveFilter<Predicate> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<Predicate> LiveFilter<Predicate> {
#[must_use]
pub fn snapshot(&self) -> Arc<Predicate> {
self.inner.snapshot()
}
}
impl<Id: Ord> LiveFilter<IdSet<Id>> {
#[must_use]
pub fn contains(&self, id: &Id) -> bool {
self.inner.read(|set| set.contains(id))
}
}
impl<Item, Predicate> SendPipe for LiveFilter<Predicate>
where
Predicate: SendPipe<In = Item, Out = Item> + Send + Sync + 'static,
Item: Send + 'static,
{
type In = Item;
type Out = Item;
type Err = Predicate::Err;
fn call(&self, input: Item) -> impl Future<Output = Result<Item, Predicate::Err>> + Send {
let predicate = self.snapshot();
async move { predicate.call(input).await }
}
}
impl<Predicate> Clone for FilterControl<Predicate> {
fn clone(&self) -> Self {
Self {
control: self.control.clone(),
}
}
}
impl<Predicate> FilterControl<Predicate> {
pub fn replace(&self, next: Predicate) {
self.control.replace(next);
}
pub fn update(&self, mutate: impl Fn(&Predicate) -> Predicate) {
self.control.update(mutate);
}
#[must_use]
pub fn snapshot(&self) -> Arc<Predicate> {
self.control.snapshot()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct IdSet<Id: Ord> {
members: BTreeSet<Id>,
}
impl<Id: Ord> IdSet<Id> {
#[must_use]
pub fn new() -> Self {
Self {
members: BTreeSet::new(),
}
}
pub fn from_ids(ids: impl IntoIterator<Item = Id>) -> Self {
ids.into_iter().collect()
}
#[must_use]
pub fn contains(&self, id: &Id) -> bool {
self.members.contains(id)
}
#[must_use]
pub fn len(&self) -> usize {
self.members.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.members.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Id> + '_ {
self.members.iter()
}
}
impl<Id: Ord + Clone> IdSet<Id> {
#[must_use]
pub fn with(&self, id: Id) -> Self {
let mut members = self.members.clone();
members.insert(id);
Self { members }
}
#[must_use]
pub fn without(&self, id: &Id) -> Self {
let mut members = self.members.clone();
members.remove(id);
Self { members }
}
}
impl<Id: Ord> Default for IdSet<Id> {
fn default() -> Self {
Self::new()
}
}
impl<Id: Ord> FromIterator<Id> for IdSet<Id> {
fn from_iter<Iter: IntoIterator<Item = Id>>(iter: Iter) -> Self {
Self {
members: iter.into_iter().collect(),
}
}
}
impl<Id: Ord + Clone + core::fmt::Debug + Send + Sync + 'static> SendPipe for IdSet<Id> {
type In = Id;
type Out = Id;
type Err = Id;
fn call(&self, id: Id) -> impl Future<Output = Result<Id, Id>> + Send {
let admitted = self.contains(&id);
async move { if admitted { Ok(id) } else { Err(id) } }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FilterUpdate<Id> {
Add(Id),
Remove(Id),
Replace(Vec<Id>),
Clear,
}
#[allow(clippy::return_self_not_must_use)]
impl<Id: Ord + Clone> FilterControl<IdSet<Id>> {
pub fn apply(&self, update: FilterUpdate<Id>) -> &Self {
match update {
FilterUpdate::Add(id) => self.update(move |set| set.with(id.clone())),
FilterUpdate::Remove(id) => self.update(move |set| set.without(&id)),
FilterUpdate::Replace(members) => self.replace(members.into_iter().collect()),
FilterUpdate::Clear => self.replace(IdSet::new()),
}
self
}
pub fn add(&self, id: Id) -> &Self {
self.apply(FilterUpdate::Add(id))
}
pub fn remove(&self, id: Id) -> &Self {
self.apply(FilterUpdate::Remove(id))
}
pub fn clear(&self) -> &Self {
self.apply(FilterUpdate::Clear)
}
pub fn replace_ids(&self, ids: impl IntoIterator<Item = Id>) -> &Self {
self.apply(FilterUpdate::Replace(ids.into_iter().collect()))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::ptr_arg)]
mod tests {
use super::*;
fn ids() -> (String, String, String) {
(
"req-7f3a9c2e1b4d".to_string(),
"req-0f43d9c0aa18".to_string(),
"req-c06db8bb5521".to_string(),
)
}
#[test]
fn decides_membership_against_the_current_set() {
let (subscribed, other, _) = ids();
let (filter, _control) = live_filter(IdSet::from_ids([subscribed.clone()]));
assert!(filter.contains(&subscribed));
assert!(!filter.contains(&other));
}
#[test]
fn replace_swaps_the_predicate_seen_by_an_existing_filter() {
let (first, second, _) = ids();
let (filter, control) = live_filter(IdSet::from_ids([first.clone()]));
assert!(filter.contains(&first));
control.replace(IdSet::from_ids([second.clone()]));
assert!(
!filter.contains(&first),
"old id no longer matches after replace"
);
assert!(filter.contains(&second), "new id matches after replace");
}
#[test]
fn add_grows_and_remove_shrinks_the_live_set() {
let (first, second, third) = ids();
let (filter, control) = live_filter(IdSet::from_ids([first.clone()]));
control.apply(FilterUpdate::Add(second.clone()));
assert!(filter.contains(&first));
assert!(filter.contains(&second));
assert!(!filter.contains(&third));
control.apply(FilterUpdate::Remove(first.clone()));
assert!(!filter.contains(&first), "removed id stops matching");
assert!(filter.contains(&second), "untouched id still matches");
}
#[test]
fn clear_matches_nothing_then_replace_reseeds() {
let (first, second, _) = ids();
let (filter, control) = live_filter(IdSet::from_ids([first.clone()]));
control.apply(FilterUpdate::Clear);
assert!(!filter.contains(&first));
assert_eq!(filter.snapshot().len(), 0);
control.apply(FilterUpdate::Replace(
[second.clone()].into_iter().collect(),
));
assert!(filter.contains(&second));
}
#[test]
fn all_clones_share_one_live_predicate() {
let (first, second, _) = ids();
let (filter, control) = live_filter(IdSet::from_ids([first.clone()]));
let subscriber_a = filter.clone();
let subscriber_b = filter.clone();
control.apply(FilterUpdate::Add(second.clone()));
assert!(subscriber_a.contains(&second));
assert!(subscriber_b.contains(&second));
}
#[test]
fn live_filter_satisfies_the_pipe_seam() {
fn admits<Predicate>(predicate: &Predicate, id: String) -> bool
where
Predicate: SendPipe<In = String, Out = String>,
{
let mut call = core::pin::pin!(SendPipe::call(predicate, id));
let waker = core::task::Waker::noop();
let mut context = core::task::Context::from_waker(waker);
loop {
if let core::task::Poll::Ready(outcome) = call.as_mut().poll(&mut context) {
return outcome.is_ok();
}
}
}
let (subscribed, other, _) = ids();
let (filter, _control) = live_filter(IdSet::from_ids([subscribed.clone()]));
assert!(admits(&filter, subscribed));
assert!(!admits(&filter, other));
}
#[test]
fn update_derives_next_from_current() {
let (first, second, _) = ids();
let (filter, control) = live_filter(IdSet::from_ids([first.clone()]));
control.update(|set| set.with(second.clone()));
assert!(filter.contains(&first));
assert!(filter.contains(&second));
}
#[test]
fn fluent_control_sugars_chain() {
let (first, second, third) = ids();
let (filter, control) = live_filter_ids([first.clone()]);
control
.add(second.clone())
.add(third.clone())
.remove(first.clone());
assert!(!filter.contains(&first), "removed id gone");
assert!(filter.contains(&second), "added id present");
assert!(filter.contains(&third), "added id present");
}
#[test]
fn id_set_config_mirror_round_trips() {
let (first, second, _) = ids();
let set = IdSet::from_ids([first, second]);
let serialized = serde_json::to_string(&set).unwrap();
assert!(
serialized.starts_with('['),
"an id-set is a bare array of ids"
);
let restored: IdSet<String> = serde_json::from_str(&serialized).unwrap();
assert_eq!(restored, set, "config mirror round-trips");
}
#[test]
fn filter_update_is_a_serializable_control_message() {
let (_, _, third) = ids();
let update = FilterUpdate::Add(third);
let serialized = serde_json::to_string(&update).unwrap();
let restored: FilterUpdate<String> = serde_json::from_str(&serialized).unwrap();
assert_eq!(
restored, update,
"control message round-trips over the wire"
);
}
}