use crate::event::{EventRef, Tags};
pub use tephra_types::{AppendCondition, Query, QueryItem};
pub trait Matches {
fn matches(&self, event: EventRef<'_>) -> bool;
}
impl Matches for QueryItem {
fn matches(&self, event: EventRef<'_>) -> bool {
let type_ok =
self.types.is_empty() || self.types.iter().any(|t| t.as_str() == event.event_type());
type_ok && tags_contained(&self.tags, event)
}
}
impl Matches for Query {
fn matches(&self, event: EventRef<'_>) -> bool {
match self {
Query::All => true,
Query::Items(items) => items.iter().any(|item| item.matches(event)),
}
}
}
fn tags_contained(required: &Tags, event: EventRef<'_>) -> bool {
let mut event_tags = event.tags();
let mut current = event_tags.next();
for req in required.iter() {
let req = req.as_str();
loop {
match current {
Some(t) if t < req => current = event_tags.next(),
Some(t) if t == req => {
current = event_tags.next();
break;
}
_ => return false,
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Event, EventType, Tag};
use smallvec::SmallVec;
fn ty(s: &str) -> EventType {
EventType::new(s).unwrap()
}
fn tags(items: &[&str]) -> Tags {
Tags::new(
items
.iter()
.map(|s| Tag::new(s).unwrap())
.collect::<SmallVec<[Tag; 4]>>(),
)
.unwrap()
}
fn event(type_str: &str, tag_strs: &[&str]) -> Event {
Event::new(&ty(type_str), &tags(tag_strs), b"").unwrap()
}
#[test]
fn item_matches_one_of_listed_types() {
let item = QueryItem::of_types(vec![ty("Registered"), ty("Deregistered")]);
assert!(item.matches(event("Registered", &[]).as_ref()));
assert!(item.matches(event("Deregistered", &[]).as_ref()));
assert!(!item.matches(event("Renamed", &[]).as_ref()));
}
#[test]
fn empty_types_matches_any_type() {
let item = QueryItem::with_tags(tags(&["course:c1"]));
assert!(item.matches(event("Anything", &["course:c1"]).as_ref()));
assert!(item.matches(event("Whatever", &["course:c1"]).as_ref()));
assert!(!item.matches(event("Anything", &["course:c2"]).as_ref()));
}
#[test]
fn empty_item_matches_everything() {
let item = QueryItem::default();
assert!(item.matches(event("A", &[]).as_ref()));
assert!(item.matches(event("B", &["x:1", "y:2"]).as_ref()));
}
#[test]
fn item_requires_all_tags_present() {
let item = QueryItem::new(vec![ty("Enrolled")], tags(&["course:c1", "student:s1"]));
assert!(item.matches(event("Enrolled", &["course:c1", "student:s1"]).as_ref()));
assert!(item.matches(event("Enrolled", &["course:c1", "extra:e", "student:s1"]).as_ref()));
assert!(!item.matches(event("Enrolled", &["course:c1"]).as_ref()));
assert!(!item.matches(event("Enrolled", &["student:s1"]).as_ref()));
assert!(!item.matches(event("Other", &["course:c1", "student:s1"]).as_ref()));
}
#[test]
fn tag_containment_boundary_cases() {
let none = QueryItem::with_tags(Tags::empty());
assert!(none.matches(event("T", &[]).as_ref()));
assert!(none.matches(event("T", &["a:1"]).as_ref()));
let low = QueryItem::with_tags(tags(&["a:0"]));
assert!(!low.matches(event("T", &["b:1", "c:2"]).as_ref()));
let high = QueryItem::with_tags(tags(&["z:9"]));
assert!(!high.matches(event("T", &["a:1", "b:2"]).as_ref()));
assert!(!high.matches(event("T", &[]).as_ref()));
}
#[test]
fn query_ors_across_items() {
let q = Query::items(vec![
QueryItem::of_types(vec![ty("Registered")]),
QueryItem::with_tags(tags(&["course:c1"])),
]);
assert!(q.matches(event("Registered", &[]).as_ref()));
assert!(q.matches(event("Enrolled", &["course:c1"]).as_ref()));
assert!(q.matches(event("Registered", &["course:c1"]).as_ref()));
assert!(!q.matches(event("Enrolled", &["course:c2"]).as_ref()));
}
#[test]
fn empty_items_query_matches_nothing() {
let q = Query::items(Vec::new());
assert!(!q.matches(event("Anything", &["x:1"]).as_ref()));
}
#[test]
fn all_query_matches_everything() {
let q = Query::all();
assert!(q.matches(event("A", &[]).as_ref()));
assert!(q.matches(event("B", &["x:1", "y:2"]).as_ref()));
assert_eq!(q, Query::All);
}
#[test]
fn single_item_query_helper() {
let q = Query::item(QueryItem::of_types(vec![ty("Ping")]));
assert!(q.matches(event("Ping", &[]).as_ref()));
assert!(!q.matches(event("Pong", &[]).as_ref()));
}
}