use crate::subscription::change_tracker::{ChangeEvent, ChangeType};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum MatchStrategy {
#[default]
Exact,
Prefix,
}
#[derive(Debug, Clone)]
pub struct StringConstraint {
pub value: String,
pub strategy: MatchStrategy,
}
impl StringConstraint {
pub fn exact(value: impl Into<String>) -> Self {
Self {
value: value.into(),
strategy: MatchStrategy::Exact,
}
}
pub fn prefix(value: impl Into<String>) -> Self {
Self {
value: value.into(),
strategy: MatchStrategy::Prefix,
}
}
pub fn matches_term(&self, term: &str) -> bool {
match self.strategy {
MatchStrategy::Exact => term == self.value,
MatchStrategy::Prefix => term.starts_with(&self.value),
}
}
}
#[derive(Debug, Clone)]
pub struct SubscriptionFilter {
pub subject_pattern: Option<StringConstraint>,
pub predicate_pattern: Option<StringConstraint>,
pub graph: Option<Option<String>>,
pub event_types: Vec<ChangeType>,
}
impl SubscriptionFilter {
pub fn all() -> Self {
Self {
subject_pattern: None,
predicate_pattern: None,
graph: None,
event_types: vec![],
}
}
pub fn inserts_only() -> Self {
Self {
subject_pattern: None,
predicate_pattern: None,
graph: None,
event_types: vec![ChangeType::Insert],
}
}
pub fn deletes_only() -> Self {
Self {
subject_pattern: None,
predicate_pattern: None,
graph: None,
event_types: vec![ChangeType::Delete],
}
}
pub fn default_graph_only() -> Self {
Self {
subject_pattern: None,
predicate_pattern: None,
graph: Some(None),
event_types: vec![],
}
}
pub fn named_graph(iri: impl Into<String>) -> Self {
Self {
subject_pattern: None,
predicate_pattern: None,
graph: Some(Some(iri.into())),
event_types: vec![],
}
}
pub fn builder() -> FilterBuilder {
FilterBuilder::new()
}
pub fn matches(&self, event: &ChangeEvent) -> bool {
if !self.event_types.is_empty() && !self.event_types.contains(&event.event_type) {
return false;
}
if let Some(ref required_graph) = self.graph {
if event.graph != *required_graph {
return false;
}
}
if let Some(ref sc) = self.subject_pattern {
if !sc.matches_term(&event.subject) {
return false;
}
}
if let Some(ref pc) = self.predicate_pattern {
if !pc.matches_term(&event.predicate) {
return false;
}
}
true
}
}
#[derive(Debug, Default)]
pub struct FilterBuilder {
subject_pattern: Option<StringConstraint>,
predicate_pattern: Option<StringConstraint>,
graph: Option<Option<String>>,
event_types: Vec<ChangeType>,
}
impl FilterBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn subject(mut self, iri: impl Into<String>) -> Self {
self.subject_pattern = Some(StringConstraint::exact(iri));
self
}
pub fn subject_prefix(mut self, prefix: impl Into<String>) -> Self {
self.subject_pattern = Some(StringConstraint::prefix(prefix));
self
}
pub fn predicate(mut self, iri: impl Into<String>) -> Self {
self.predicate_pattern = Some(StringConstraint::exact(iri));
self
}
pub fn predicate_prefix(mut self, prefix: impl Into<String>) -> Self {
self.predicate_pattern = Some(StringConstraint::prefix(prefix));
self
}
pub fn default_graph(mut self) -> Self {
self.graph = Some(None);
self
}
pub fn graph(mut self, iri: impl Into<String>) -> Self {
self.graph = Some(Some(iri.into()));
self
}
pub fn event_type(mut self, et: ChangeType) -> Self {
self.event_types.push(et);
self
}
pub fn build(self) -> SubscriptionFilter {
SubscriptionFilter {
subject_pattern: self.subject_pattern,
predicate_pattern: self.predicate_pattern,
graph: self.graph,
event_types: self.event_types,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::subscription::change_tracker::{ChangeEvent, ChangeType};
fn insert_event(subject: &str, predicate: &str, graph: Option<&str>) -> ChangeEvent {
ChangeEvent::new(
1,
ChangeType::Insert,
subject,
predicate,
"http://ex.org/o",
graph.map(|g| g.to_string()),
)
}
fn delete_event(subject: &str, predicate: &str) -> ChangeEvent {
ChangeEvent::new(
2,
ChangeType::Delete,
subject,
predicate,
"http://ex.org/o",
None,
)
}
#[test]
fn test_filter_all_matches_any_event() {
let f = SubscriptionFilter::all();
assert!(f.matches(&insert_event("s", "p", None)));
assert!(f.matches(&delete_event("s2", "p2")));
}
#[test]
fn test_filter_inserts_only() {
let f = SubscriptionFilter::inserts_only();
assert!(f.matches(&insert_event("s", "p", None)));
assert!(!f.matches(&delete_event("s", "p")));
}
#[test]
fn test_filter_deletes_only() {
let f = SubscriptionFilter::deletes_only();
assert!(!f.matches(&insert_event("s", "p", None)));
assert!(f.matches(&delete_event("s", "p")));
}
#[test]
fn test_filter_default_graph_only() {
let f = SubscriptionFilter::default_graph_only();
assert!(f.matches(&insert_event("s", "p", None)));
assert!(!f.matches(&insert_event("s", "p", Some("http://ex.org/g"))));
}
#[test]
fn test_filter_named_graph() {
let f = SubscriptionFilter::named_graph("http://ex.org/g");
assert!(f.matches(&insert_event("s", "p", Some("http://ex.org/g"))));
assert!(!f.matches(&insert_event("s", "p", None)));
assert!(!f.matches(&insert_event("s", "p", Some("http://ex.org/other"))));
}
#[test]
fn test_filter_builder_exact_subject() {
let f = SubscriptionFilter::builder()
.subject("http://ex.org/person1")
.build();
assert!(f.matches(&insert_event("http://ex.org/person1", "p", None)));
assert!(!f.matches(&insert_event("http://ex.org/person2", "p", None)));
}
#[test]
fn test_filter_builder_subject_prefix() {
let f = SubscriptionFilter::builder()
.subject_prefix("http://ex.org/person")
.build();
assert!(f.matches(&insert_event("http://ex.org/person1", "p", None)));
assert!(f.matches(&insert_event("http://ex.org/personX", "p", None)));
assert!(!f.matches(&insert_event("http://ex.org/thing1", "p", None)));
}
#[test]
fn test_filter_builder_exact_predicate() {
let f = SubscriptionFilter::builder()
.predicate("http://xmlns.com/foaf/0.1/name")
.build();
assert!(f.matches(&insert_event("s", "http://xmlns.com/foaf/0.1/name", None)));
assert!(!f.matches(&insert_event("s", "http://xmlns.com/foaf/0.1/age", None)));
}
#[test]
fn test_filter_builder_predicate_prefix() {
let f = SubscriptionFilter::builder()
.predicate_prefix("http://xmlns.com/foaf/")
.build();
assert!(f.matches(&insert_event("s", "http://xmlns.com/foaf/name", None)));
assert!(!f.matches(&insert_event(
"s",
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
None
)));
}
#[test]
fn test_filter_builder_multiple_event_types() {
let f = SubscriptionFilter::builder()
.event_type(ChangeType::Insert)
.event_type(ChangeType::Update)
.build();
let update_ev = ChangeEvent::new(3, ChangeType::Update, "s", "p", "o", None);
assert!(f.matches(&insert_event("s", "p", None)));
assert!(f.matches(&update_ev));
assert!(!f.matches(&delete_event("s", "p")));
}
#[test]
fn test_filter_builder_combined_constraints() {
let f = SubscriptionFilter::builder()
.subject("http://ex.org/alice")
.predicate_prefix("http://xmlns.com/foaf/")
.graph("http://ex.org/people")
.event_type(ChangeType::Insert)
.build();
let matching = ChangeEvent::new(
1,
ChangeType::Insert,
"http://ex.org/alice",
"http://xmlns.com/foaf/name",
"Alice",
Some("http://ex.org/people".to_string()),
);
assert!(f.matches(&matching));
let wrong_subject = ChangeEvent::new(
2,
ChangeType::Insert,
"http://ex.org/bob",
"http://xmlns.com/foaf/name",
"Bob",
Some("http://ex.org/people".to_string()),
);
assert!(!f.matches(&wrong_subject));
}
#[test]
fn test_string_constraint_exact() {
let c = StringConstraint::exact("foo");
assert!(c.matches_term("foo"));
assert!(!c.matches_term("foobar"));
assert!(!c.matches_term("bar"));
}
#[test]
fn test_string_constraint_prefix() {
let c = StringConstraint::prefix("http://ex.org/");
assert!(c.matches_term("http://ex.org/thing"));
assert!(!c.matches_term("http://other.org/thing"));
}
}