use super::Filter;
use super::nodes::{And, Cmp, CmpOp, Has, IsA, Missing, Or, Parens, Relation, Term, WildcardEq};
use super::path::Path;
use crate::haystack::encoding::zinc::decode::id::Id;
use crate::haystack::val::{Ref, Symbol, Value};
use std::marker::PhantomData;
pub trait IntoFilterPath {
fn into_filter_path(self) -> Path;
}
impl IntoFilterPath for &str {
fn into_filter_path(self) -> Path {
Path::from(self)
}
}
impl IntoFilterPath for &[&str] {
fn into_filter_path(self) -> Path {
Path::from(self.iter().map(|s| Id::from(*s)).collect::<Vec<_>>())
}
}
impl<const N: usize> IntoFilterPath for [&str; N] {
fn into_filter_path(self) -> Path {
Path::from(self.iter().map(|s| Id::from(*s)).collect::<Vec<_>>())
}
}
impl IntoFilterPath for Vec<&str> {
fn into_filter_path(self) -> Path {
Path::from(self.iter().map(|s| Id::from(*s)).collect::<Vec<_>>())
}
}
pub struct NeedsTerm;
pub struct HasTerm;
pub struct FilterBuilder<S = NeedsTerm> {
_state: PhantomData<S>,
ands: Vec<And>,
current_terms: Vec<Term>,
paren_stack: Vec<(Vec<And>, Vec<Term>)>,
}
impl FilterBuilder<NeedsTerm> {
pub fn new() -> Self {
Self::default()
}
}
impl Default for FilterBuilder<NeedsTerm> {
fn default() -> Self {
FilterBuilder {
_state: PhantomData,
ands: Vec::new(),
current_terms: Vec::new(),
paren_stack: Vec::new(),
}
}
}
impl<S> FilterBuilder<S> {
fn transition<T>(self) -> FilterBuilder<T> {
FilterBuilder {
_state: PhantomData,
ands: self.ands,
current_terms: self.current_terms,
paren_stack: self.paren_stack,
}
}
fn flush_and(&mut self) {
if !self.current_terms.is_empty() {
self.ands.push(And {
terms: std::mem::take(&mut self.current_terms),
});
}
}
fn cmp(mut self, path: impl IntoFilterPath, op: CmpOp, value: Value) -> FilterBuilder<HasTerm> {
self.current_terms.push(Term::Cmp(Cmp {
path: path.into_filter_path(),
op,
value,
}));
self.transition()
}
}
impl FilterBuilder<NeedsTerm> {
pub fn start_parens(mut self) -> FilterBuilder<NeedsTerm> {
self.paren_stack.push((
std::mem::take(&mut self.ands),
std::mem::take(&mut self.current_terms),
));
self.transition()
}
pub fn has(mut self, path: impl IntoFilterPath) -> FilterBuilder<HasTerm> {
self.current_terms.push(Term::Has(Has {
path: path.into_filter_path(),
}));
self.transition()
}
pub fn not(mut self, path: impl IntoFilterPath) -> FilterBuilder<HasTerm> {
self.current_terms.push(Term::Missing(Missing {
path: path.into_filter_path(),
}));
self.transition()
}
pub fn is_a(mut self, symbol: impl Into<Symbol>) -> FilterBuilder<HasTerm> {
self.current_terms.push(Term::IsA(IsA {
symbol: symbol.into(),
}));
self.transition()
}
pub fn wildcard_eq(
mut self,
id: impl IntoFilterPath,
ref_value: Ref,
) -> FilterBuilder<HasTerm> {
self.current_terms.push(Term::WildcardEq(WildcardEq {
id: id.into_filter_path(),
ref_value,
}));
self.transition()
}
pub fn relation(
mut self,
rel: impl Into<Symbol>,
term: Option<Symbol>,
ref_value: Option<Ref>,
) -> FilterBuilder<HasTerm> {
self.current_terms.push(Term::Relation(Relation {
rel: rel.into(),
rel_term: term,
ref_value,
}));
self.transition()
}
pub fn eq(self, path: impl IntoFilterPath, value: Value) -> FilterBuilder<HasTerm> {
self.cmp(path, CmpOp::Eq, value)
}
pub fn ne(self, path: impl IntoFilterPath, value: Value) -> FilterBuilder<HasTerm> {
self.cmp(path, CmpOp::NotEq, value)
}
pub fn lt(self, path: impl IntoFilterPath, value: Value) -> FilterBuilder<HasTerm> {
self.cmp(path, CmpOp::LessThan, value)
}
pub fn lte(self, path: impl IntoFilterPath, value: Value) -> FilterBuilder<HasTerm> {
self.cmp(path, CmpOp::LessThanEq, value)
}
pub fn gt(self, path: impl IntoFilterPath, value: Value) -> FilterBuilder<HasTerm> {
self.cmp(path, CmpOp::GreatThan, value)
}
pub fn gte(self, path: impl IntoFilterPath, value: Value) -> FilterBuilder<HasTerm> {
self.cmp(path, CmpOp::GreatThanEq, value)
}
pub fn filter(mut self, filter: Filter) -> FilterBuilder<HasTerm> {
self.current_terms
.push(Term::Parens(Parens { or: filter.or }));
self.transition()
}
}
impl FilterBuilder<HasTerm> {
pub fn and(self) -> FilterBuilder<NeedsTerm> {
self.transition()
}
pub fn or(mut self) -> FilterBuilder<NeedsTerm> {
self.flush_and();
self.transition()
}
pub fn end_parens(mut self) -> FilterBuilder<HasTerm> {
if let Some((outer_ands, outer_terms)) = self.paren_stack.pop() {
self.flush_and();
let inner_or = Or {
ands: std::mem::take(&mut self.ands),
};
self.ands = outer_ands;
self.current_terms = outer_terms;
self.current_terms
.push(Term::Parens(Parens { or: inner_or }));
}
self
}
pub fn build(mut self) -> Filter {
self.flush_and();
Filter {
or: Or { ands: self.ands },
}
}
}
impl From<FilterBuilder<HasTerm>> for Filter {
fn from(builder: FilterBuilder<HasTerm>) -> Self {
builder.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::val::{Date, Ref, Time, Value};
#[test]
fn test_builder_has() {
let f = FilterBuilder::new().has("site").build();
assert_eq!(f.to_string(), "site");
}
#[test]
fn test_builder_not() {
let f = FilterBuilder::new().not("site").build();
assert_eq!(f.to_string(), "not site");
}
#[test]
fn test_builder_and() {
let f = FilterBuilder::new().has("site").and().has("equip").build();
assert_eq!(f.to_string(), "site and equip");
}
#[test]
fn test_builder_or() {
let f = FilterBuilder::new().has("site").or().has("equip").build();
assert_eq!(f.to_string(), "site or equip");
}
#[test]
fn test_builder_and_or_combined() {
let f = FilterBuilder::new()
.has("site")
.and()
.has("equip")
.or()
.has("point")
.build();
assert_eq!(f.to_string(), "site and equip or point");
}
#[test]
fn test_builder_is_a() {
let f = FilterBuilder::new().is_a("point").build();
assert_eq!(f.to_string(), "^point");
}
#[test]
fn test_builder_parens() {
let f = FilterBuilder::new()
.start_parens()
.has("equip")
.or()
.has("point")
.end_parens()
.build();
assert_eq!(f.to_string(), "(equip or point)");
}
#[test]
fn test_builder_parens_with_outer_term() {
let f = FilterBuilder::new()
.start_parens()
.has("equip")
.or()
.has("point")
.end_parens()
.and()
.eq("siteRef", Value::make_ref("mySite"))
.build();
assert_eq!(f.to_string(), "(equip or point) and siteRef == @mySite");
}
#[test]
fn test_builder_eq_str() {
let f = FilterBuilder::new()
.eq("dis", Value::make_str("Chiller"))
.build();
assert_eq!(f.to_string(), r#"dis == "Chiller""#);
}
#[test]
fn test_builder_eq_number() {
let f = FilterBuilder::new()
.eq("num", Value::make_number(42.0))
.build();
assert_eq!(f.to_string(), "num == 42");
}
#[test]
fn test_builder_ne() {
let f = FilterBuilder::new()
.ne("num", Value::make_number(42.0))
.build();
assert_eq!(f.to_string(), "num != 42");
}
#[test]
fn test_builder_lt() {
let f = FilterBuilder::new()
.lt("num", Value::make_number(10.0))
.build();
assert_eq!(f.to_string(), "num < 10");
}
#[test]
fn test_builder_lte() {
let f = FilterBuilder::new()
.lte("num", Value::make_number(10.0))
.build();
assert_eq!(f.to_string(), "num <= 10");
}
#[test]
fn test_builder_gt() {
let f = FilterBuilder::new()
.gt("num", Value::make_number(10.0))
.build();
assert_eq!(f.to_string(), "num > 10");
}
#[test]
fn test_builder_gte() {
let f = FilterBuilder::new()
.gte("num", Value::make_number(10.0))
.build();
assert_eq!(f.to_string(), "num >= 10");
}
#[test]
fn test_builder_eq_ref() {
let f = FilterBuilder::new()
.eq("siteRef", Value::make_ref("mySite"))
.build();
assert_eq!(f.to_string(), "siteRef == @mySite");
}
#[test]
fn test_builder_eq_bool() {
let f = FilterBuilder::new()
.eq("active", Value::make_bool(true))
.build();
assert_eq!(f.to_string(), "active == true");
}
#[test]
fn test_builder_eq_date() {
let date = Date::from_ymd(2024, 3, 15).expect("date");
let f = FilterBuilder::new()
.eq("lastMod", Value::make_date(date))
.build();
assert_eq!(f.to_string(), "lastMod == 2024-03-15");
}
#[test]
fn test_builder_eq_time() {
let time = Time::from_hms(12, 30, 0).expect("time");
let f = FilterBuilder::new()
.eq("startTime", Value::make_time(time))
.build();
assert_eq!(f.to_string(), "startTime == 12:30:00");
}
#[test]
fn test_builder_multi_segment_path_has() {
let f = FilterBuilder::new().has(["siteRef", "dis"]).build();
assert_eq!(f.to_string(), "siteRef->dis");
}
#[test]
fn test_builder_multi_segment_path_eq() {
let f = FilterBuilder::new()
.eq(["siteRef", "dis"], Value::make_str("Main"))
.build();
assert_eq!(f.to_string(), r#"siteRef->dis == "Main""#);
}
#[test]
fn test_builder_wildcard_eq() {
let f = FilterBuilder::new()
.wildcard_eq("siteRef", Ref::from("mySite"))
.build();
assert_eq!(f.to_string(), "siteRef *== @mySite");
}
#[test]
fn test_builder_relation_simple() {
let f = FilterBuilder::new()
.relation("containedBy", None, None)
.build();
assert_eq!(f.to_string(), "containedBy?");
}
#[test]
fn test_builder_relation_with_ref() {
let f = FilterBuilder::new()
.relation("containedBy", None, Some(Ref::from("mySite")))
.build();
assert_eq!(f.to_string(), "containedBy? @mySite");
}
#[test]
fn test_builder_relation_with_term_and_ref() {
let f = FilterBuilder::new()
.relation(
"containedBy",
Some(Symbol::from("site")),
Some(Ref::from("mySite")),
)
.build();
assert_eq!(f.to_string(), "containedBy? ^site @mySite");
}
#[test]
fn test_builder_embed_filter() {
let inner = Filter::try_from("equip or point").unwrap();
let f = FilterBuilder::new().filter(inner).and().has("site").build();
assert_eq!(f.to_string(), "(equip or point) and site");
}
#[test]
fn test_filter_from_builder() {
let builder: FilterBuilder<HasTerm> = FilterBuilder::new().has("site").and().has("equip");
let f: Filter = builder.into();
assert_eq!(f.to_string(), "site and equip");
}
#[test]
fn test_builder_round_trip_simple() {
let built = FilterBuilder::new()
.has("site")
.and()
.eq("dis", Value::make_str("Test"))
.build();
let parsed = Filter::try_from(r#"site and dis == "Test""#).unwrap();
assert_eq!(built.to_string(), parsed.to_string());
}
#[test]
fn test_builder_round_trip_parens() {
let built = FilterBuilder::new()
.start_parens()
.has("equip")
.or()
.has("point")
.end_parens()
.and()
.has("site")
.build();
let parsed = Filter::try_from("(equip or point) and site").unwrap();
assert_eq!(built.to_string(), parsed.to_string());
}
#[test]
fn test_builder_round_trip_or() {
let built = FilterBuilder::new()
.has("site")
.or()
.has("equip")
.or()
.has("point")
.build();
let parsed = Filter::try_from("site or equip or point").unwrap();
assert_eq!(built.to_string(), parsed.to_string());
}
}