use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;
use std::marker::PhantomData;
use num_traits::AsPrimitive;
use uuid::Uuid;
use crate::metadata::{AnnotationValue, Field, ReadMetadata, WeightValue};
pub trait MetadataFilter<IF, NF, KF, LF, WF, AF, N, K, L, W, WV, A, AV>
where
IF: FieldsFilter<Uuid>,
NF: FieldsFilter<N>,
KF: FieldsFilter<K>,
LF: FieldsFilter<L>,
WF: FieldValuesFilter<W, WV>,
AF: FieldValuesFilter<A, AV>,
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn matches_metadata<'a, M: ReadMetadata<'a, N, K, L, W, WV, A, AV>>(
&'a self,
meta: &'a M,
) -> bool
where
N: 'a,
K: 'a,
L: 'a,
W: 'a,
WV: 'a,
A: 'a,
AV: 'a,
LF: 'a,
WF: 'a,
AF: 'a,
{
self.id_filters().all(|f| f.match_one(meta.id()))
&& self
.name_filters()
.all(|f| f.matches(meta.name().into_iter()))
&& self
.kind_filters()
.all(|f| f.matches(meta.kind().into_iter()))
&& self.label_filters().all(|f| f.matches(meta.labels()))
&& self.weight_filters().all(|f| f.matches(meta.weights()))
&& self
.annotation_filters()
.all(|f| f.matches(meta.annotations()))
}
fn id_filters<'a>(&'a self) -> impl Iterator<Item = &'a IF>
where
IF: 'a;
fn name_filters<'a>(&'a self) -> impl Iterator<Item = &'a NF>
where
NF: 'a;
fn kind_filters<'a>(&'a self) -> impl Iterator<Item = &'a KF>
where
KF: 'a;
fn label_filters<'a>(&'a self) -> impl Iterator<Item = &'a LF>
where
LF: 'a;
fn weight_filters<'a>(&'a self) -> impl Iterator<Item = &'a WF>
where
WF: 'a;
fn annotation_filters<'a>(&'a self) -> impl Iterator<Item = &'a AF>
where
AF: 'a;
}
pub trait FieldsFilter<F: Field> {
fn matches<'a, Fs: Iterator<Item = &'a F>>(&'a self, fields: Fs) -> bool
where
F: 'a,
{
match self.matching_mode() {
MatchingMode::All => self.match_all(fields),
MatchingMode::Any => self.match_any(fields),
MatchingMode::None => self.match_none(fields),
}
}
fn matching_mode(&self) -> &MatchingMode;
fn match_one(&self, field: &F) -> bool;
fn match_all<'a, Fs: Iterator<Item = &'a F>>(&'a self, fields: Fs) -> bool
where
F: 'a,
{
fields
.fold(BTreeSet::new(), |mut matched, f| {
if self.match_one(f) {
matched.insert(f);
}
matched
})
.len()
== self.max_matches()
}
fn max_matches(&self) -> usize;
fn match_any<'a, Fs: Iterator<Item = &'a F>>(&'a self, mut fields: Fs) -> bool
where
F: 'a,
{
fields.any(|f| self.match_one(f))
}
fn match_none<'a, Fs: Iterator<Item = &'a F>>(&'a self, fields: Fs) -> bool
where
F: 'a,
{
!self.match_any(fields)
}
}
pub trait FieldValuesFilter<F: Field, V> {
fn matches<'a, I: Iterator<Item = (&'a F, &'a V)>>(&'a self, items: I) -> bool
where
F: 'a,
V: 'a,
{
match self.matching_mode() {
MatchingMode::All => self.match_all(items),
MatchingMode::Any => self.match_any(items),
MatchingMode::None => self.match_none(items),
}
}
fn matching_mode(&self) -> &MatchingMode;
fn match_one(&self, field: &F, value: &V) -> bool;
fn match_all<'a, I: Iterator<Item = (&'a F, &'a V)>>(&'a self, items: I) -> bool
where
F: 'a,
V: 'a,
{
items
.fold(BTreeSet::new(), |mut matched, (f, v)| {
if self.match_one(f, v) {
matched.insert(f);
}
matched
})
.len()
== self.max_matches()
}
fn max_matches(&self) -> usize;
fn match_any<'a, I: Iterator<Item = (&'a F, &'a V)>>(&'a self, mut items: I) -> bool
where
F: 'a,
V: 'a,
{
items.any(|(f, v)| self.match_one(f, v))
}
fn match_none<'a, I: Iterator<Item = (&'a F, &'a V)>>(&'a self, items: I) -> bool
where
F: 'a,
V: 'a,
{
!self.match_any(items)
}
}
pub trait ValueFilter<V> {
fn matches<'a>(&'a self, value: &'a V) -> bool;
}
impl<T> ValueFilter<T> for () {
fn matches<'a>(&'a self, _: &'a T) -> bool {
true
}
}
#[derive(Clone, Debug, PartialEq, bon::Builder)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default, rename_all = "camelCase")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
pub struct BTreeMetadataFilter<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
#[builder(default)]
pub id_filters: Vec<BTreeFieldsFilter<Uuid>>,
#[builder(default)]
pub name_filters: Vec<BTreeFieldsFilter<N>>,
#[builder(default)]
pub kind_filters: Vec<BTreeFieldsFilter<K>>,
#[builder(default)]
pub label_filters: Vec<BTreeFieldsFilter<L>>,
#[builder(default)]
pub weight_filters: Vec<BTreeFieldValuesFilter<W, Domain1D<WV>, WV>>,
#[builder(default)]
pub annotation_filters: Vec<BTreeFieldValuesFilter<A, (), AV>>, }
impl<N, K, L, W, WV, A, AV> Default for BTreeMetadataFilter<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn default() -> Self {
Self {
id_filters: Vec::new(),
name_filters: Vec::new(),
kind_filters: Vec::new(),
label_filters: Vec::new(),
weight_filters: Vec::new(),
annotation_filters: Vec::new(),
}
}
}
impl<N, K, L, W, WV, A, AV>
MetadataFilter<
BTreeFieldsFilter<Uuid>,
BTreeFieldsFilter<N>,
BTreeFieldsFilter<K>,
BTreeFieldsFilter<L>,
BTreeFieldValuesFilter<W, Domain1D<WV>, WV>,
BTreeFieldValuesFilter<A, (), AV>,
N,
K,
L,
W,
WV,
A,
AV,
> for BTreeMetadataFilter<N, K, L, W, WV, A, AV>
where
N: Field,
K: Field,
L: Field,
W: Field,
WV: WeightValue,
A: Field,
AV: AnnotationValue,
{
fn id_filters<'a>(&'a self) -> impl Iterator<Item = &'a BTreeFieldsFilter<Uuid>>
where
BTreeFieldsFilter<Uuid>: 'a,
{
self.id_filters.iter()
}
fn name_filters<'a>(&'a self) -> impl Iterator<Item = &'a BTreeFieldsFilter<N>>
where
BTreeFieldsFilter<N>: 'a,
{
self.name_filters.iter()
}
fn kind_filters<'a>(&'a self) -> impl Iterator<Item = &'a BTreeFieldsFilter<K>>
where
BTreeFieldsFilter<K>: 'a,
{
self.kind_filters.iter()
}
fn label_filters<'a>(&'a self) -> impl Iterator<Item = &'a BTreeFieldsFilter<L>>
where
BTreeFieldsFilter<L>: 'a,
{
self.label_filters.iter()
}
fn weight_filters<'a>(
&'a self,
) -> impl Iterator<Item = &'a BTreeFieldValuesFilter<W, Domain1D<WV>, WV>>
where
BTreeFieldValuesFilter<W, Domain1D<WV>, WV>: 'a,
{
self.weight_filters.iter()
}
fn annotation_filters<'a>(
&'a self,
) -> impl Iterator<Item = &'a BTreeFieldValuesFilter<A, (), AV>>
where
BTreeFieldsFilter<A>: 'a,
{
self.annotation_filters.iter()
}
}
#[derive(Clone, Debug, PartialEq, bon::Builder)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default, rename_all = "camelCase")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
pub struct BTreeFieldsFilter<F>
where
F: Field,
{
#[builder(default)]
fields: BTreeSet<F>,
#[builder(default)]
mode: MatchingMode,
}
impl<F: Field> Default for BTreeFieldsFilter<F> {
fn default() -> Self {
Self {
fields: BTreeSet::<F>::new(),
mode: MatchingMode::Any,
}
}
}
impl<F: Field> FieldsFilter<F> for BTreeFieldsFilter<F> {
fn max_matches(&self) -> usize {
self.fields.len()
}
fn match_one(&self, field: &F) -> bool {
self.fields.contains(field)
}
fn matching_mode(&self) -> &MatchingMode {
&self.mode
}
}
#[derive(Clone, Debug, PartialEq, bon::Builder)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default, rename_all = "camelCase")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
pub struct BTreeFieldValuesFilter<F, VF, V>
where
F: Field,
VF: ValueFilter<V>,
{
#[builder(default)]
pub fields: BTreeMap<F, VF>,
#[builder(default)]
pub mode: MatchingMode,
#[builder(default)]
_value: PhantomData<V>,
}
impl<F: Field, VF: ValueFilter<V>, V> Default for BTreeFieldValuesFilter<F, VF, V> {
fn default() -> Self {
Self {
fields: BTreeMap::new(),
_value: PhantomData,
mode: MatchingMode::Any,
}
}
}
impl<F: Field, V> From<BTreeFieldsFilter<F>> for BTreeFieldValuesFilter<F, (), V> {
fn from(value: BTreeFieldsFilter<F>) -> Self {
Self {
fields: value.fields.into_iter().map(|f| (f, ())).collect(),
_value: PhantomData,
mode: value.mode,
}
}
}
impl<F: Field, VF: ValueFilter<V>, V> FieldValuesFilter<F, V> for BTreeFieldValuesFilter<F, VF, V> {
fn matching_mode(&self) -> &MatchingMode {
&self.mode
}
fn match_one(&self, field: &F, value: &V) -> bool {
self.fields
.get(field)
.map(|vf| vf.matches(value))
.unwrap_or_default()
}
fn max_matches(&self) -> usize {
self.fields.len()
}
}
#[derive(Clone, Debug, PartialEq, bon::Builder)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default, rename_all = "camelCase")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
pub struct Domain1D<WV>
where
WV: PartialEq + PartialOrd,
{
lower: Option<WV>,
upper: Option<WV>,
}
impl<WV: PartialEq + PartialOrd> Default for Domain1D<WV> {
fn default() -> Self {
Domain1D {
lower: None,
upper: None,
}
}
}
impl<T: AsPrimitive<Q>, Q: 'static + Copy + PartialEq + PartialOrd> From<(T, T)> for Domain1D<Q> {
fn from(value: (T, T)) -> Self {
Domain1D::builder()
.lower(value.0.as_())
.upper(value.1.as_())
.build()
}
}
impl<WV: PartialEq + PartialOrd> ValueFilter<WV> for Domain1D<WV> {
fn matches<'a>(&'a self, value: &'a WV) -> bool {
match self {
Self {
lower: Some(lb),
upper: Some(ub),
} => value >= lb && value <= ub,
Self {
lower: Some(lb),
upper: None,
} => value >= lb,
Self {
lower: None,
upper: Some(ub),
} => value <= ub,
_ => true,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(rename_all = "camelCase")
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "reactive", derive(reactive_stores::Store))]
pub enum MatchingMode {
#[default]
Any,
All,
None,
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::metadata::tests::{A, K, L, N, TestMeta, W};
#[test]
fn test_domain1d_from() {
let dom = Domain1D::<f64>::from((1234, 4678));
assert_eq!(dom.lower, Some(1234.0));
assert_eq!(dom.upper, Some(4678.0));
}
#[test]
fn test_btree_fields_filter() {
let filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([N::Foo, N::Bar]),
mode: MatchingMode::Any,
};
assert!(filter.matches(vec![&N::Foo].into_iter()));
assert!(filter.matches(vec![&N::Bar].into_iter()));
assert!(filter.matches(vec![&N::Foo, &N::Baz].into_iter()));
assert!(!filter.matches(vec![&N::Baz].into_iter()));
}
#[test]
fn test_btree_fields_filter_all_mode() {
let filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([N::Foo, N::Bar]),
mode: MatchingMode::All,
};
assert!(filter.matches(vec![&N::Foo, &N::Bar].into_iter()));
assert!(!filter.matches(vec![&N::Foo].into_iter()));
assert!(!filter.matches(vec![&N::Bar].into_iter()));
}
#[test]
fn test_btree_fields_filter_none_mode() {
let filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([N::Foo, N::Bar]),
mode: MatchingMode::None,
};
assert!(filter.matches(vec![&N::Baz].into_iter()));
assert!(!filter.matches(vec![&N::Foo].into_iter()));
assert!(!filter.matches(vec![&N::Bar].into_iter()));
assert!(!filter.matches(vec![&N::Foo, &N::Bar].into_iter()));
}
#[test]
fn test_btree_field_values_filter() {
let domain = Domain1D {
lower: Some(1.0),
upper: Some(3.0),
};
let filter = BTreeFieldValuesFilter {
fields: BTreeMap::from_iter([(W::A, domain)]),
_value: PhantomData,
mode: MatchingMode::Any,
};
assert!(filter.matches(vec![(&W::A, &2.0)].into_iter()));
assert!(!filter.matches(vec![(&W::A, &0.5)].into_iter()));
assert!(!filter.matches(vec![(&W::A, &3.5)].into_iter()));
}
#[test]
fn test_btree_field_values_filter_all_mode() {
let domain1 = Domain1D {
lower: Some(1.0),
upper: Some(3.0),
};
let domain2 = Domain1D {
lower: Some(2.0),
upper: Some(4.0),
};
let filter = BTreeFieldValuesFilter {
fields: BTreeMap::from_iter([(W::A, domain1), (W::B, domain2)]),
_value: PhantomData,
mode: MatchingMode::All,
};
assert!(filter.matches(vec![(&W::A, &2.0), (&W::B, &3.0)].into_iter()));
assert!(!filter.matches(vec![(&W::A, &2.0)].into_iter()));
assert!(!filter.matches(vec![(&W::B, &3.0)].into_iter()));
}
#[test]
fn test_btree_field_values_filter_none_mode() {
let domain = Domain1D {
lower: Some(1.0),
upper: Some(3.0),
};
let filter = BTreeFieldValuesFilter {
fields: BTreeMap::from_iter([(W::A, domain)]),
_value: PhantomData,
mode: MatchingMode::None,
};
assert!(filter.matches(vec![(&W::B, &2.0)].into_iter()));
assert!(!filter.matches(vec![(&W::A, &2.0)].into_iter()));
}
#[test]
fn test_btree_metadata_filter() {
let name_filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([N::Foo, N::Bar]),
mode: MatchingMode::Any,
};
let kind_filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([K::A, K::B]),
mode: MatchingMode::Any,
};
let label_filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([L::A, L::B]),
mode: MatchingMode::Any,
};
let weight_domain = Domain1D {
lower: Some(1.0),
upper: Some(3.0),
};
let weight_filter = BTreeFieldValuesFilter {
fields: BTreeMap::from_iter([(W::A, weight_domain)]),
_value: PhantomData,
mode: MatchingMode::Any,
};
let annotation_filter = BTreeFieldValuesFilter::builder()
.fields(bon::map! { A::A: ()})
.mode(MatchingMode::Any)
.build();
let metadata_filter = BTreeMetadataFilter {
id_filters: vec![],
name_filters: vec![name_filter],
kind_filters: vec![kind_filter],
label_filters: vec![label_filter],
weight_filters: vec![weight_filter],
annotation_filters: vec![annotation_filter],
};
let foo = TestMeta::foo();
let bar = TestMeta::bar();
let baz = TestMeta::baz();
let quux = TestMeta::quux();
assert!(metadata_filter.matches_metadata(&foo));
assert!(!metadata_filter.matches_metadata(&bar));
assert!(!metadata_filter.matches_metadata(&baz));
assert!(!metadata_filter.matches_metadata(&quux));
assert!(
metadata_filter
.id_filters()
.all(|f| f.matches(vec![&foo.id].into_iter()))
);
assert!(
metadata_filter
.name_filters()
.all(|f| f.matches(foo.name().into_iter()))
);
assert!(
metadata_filter
.kind_filters()
.all(|f| f.matches(foo.kind().into_iter()))
);
assert!(
metadata_filter
.label_filters()
.all(|f| f.matches(foo.labels()))
);
assert!(
metadata_filter
.weight_filters()
.all(|f| f.matches(foo.weights()))
);
assert!(
metadata_filter
.annotation_filters()
.all(|f| f.matches(foo.annotations()))
);
let none_filter = BTreeFieldsFilter {
fields: BTreeSet::from_iter([N::Baz]),
mode: MatchingMode::None,
};
assert!(none_filter.matches(foo.name().into_iter()));
assert!(!none_filter.matches(baz.name().into_iter()));
}
#[test]
fn test_domain1d_value_filter() {
let domain = Domain1D {
lower: Some(1.0),
upper: Some(3.0),
};
assert!(domain.matches(&2.0));
assert!(!domain.matches(&0.5));
assert!(!domain.matches(&3.5));
let lower_only = Domain1D {
lower: Some(1.0),
upper: None,
};
assert!(lower_only.matches(&2.0));
assert!(!lower_only.matches(&0.5));
let upper_only = Domain1D {
lower: None,
upper: Some(3.0),
};
assert!(upper_only.matches(&2.0));
assert!(!upper_only.matches(&3.5));
let no_bounds = Domain1D {
lower: None,
upper: None,
};
assert!(no_bounds.matches(&2.0));
assert!(no_bounds.matches(&0.5));
assert!(no_bounds.matches(&3.5));
}
}