use crate::model::Model;
use crate::query::condition::{Condition, FilterValue, Leaf, LookupOp};
use crate::query::field::IntoFilterValue;
use crate::query::q::{CompoundOp, Q};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Lookup<V> {
Eq(V),
Neq(V),
Gt(V),
Gte(V),
Lt(V),
Lte(V),
In(Vec<V>),
NotIn(Vec<V>),
IsNull,
IsNotNull,
Contains(String),
StartsWith(String),
EndsWith(String),
Between(V, V),
Regex(String),
IRegex(String),
}
impl<V: IntoFilterValue> Lookup<V> {
pub(crate) fn into_op_value(self) -> (LookupOp, FilterValue) {
match self {
Lookup::Eq(v) => (LookupOp::Eq, v.into_filter_value()),
Lookup::Neq(v) => (LookupOp::Neq, v.into_filter_value()),
Lookup::Gt(v) => (LookupOp::Gt, v.into_filter_value()),
Lookup::Gte(v) => (LookupOp::Gte, v.into_filter_value()),
Lookup::Lt(v) => (LookupOp::Lt, v.into_filter_value()),
Lookup::Lte(v) => (LookupOp::Lte, v.into_filter_value()),
Lookup::In(vs) => (
LookupOp::In,
FilterValue::List(vs.into_iter().map(|v| v.into_filter_value()).collect()),
),
Lookup::NotIn(vs) => (
LookupOp::NotIn,
FilterValue::List(vs.into_iter().map(|v| v.into_filter_value()).collect()),
),
Lookup::IsNull => (LookupOp::IsNull, FilterValue::Null),
Lookup::IsNotNull => (LookupOp::IsNotNull, FilterValue::Null),
Lookup::Contains(s) => (LookupOp::IContains, FilterValue::String(s)),
Lookup::StartsWith(s) => (LookupOp::IStartsWith, FilterValue::String(s)),
Lookup::EndsWith(s) => (LookupOp::IEndsWith, FilterValue::String(s)),
Lookup::Between(a, b) => (
LookupOp::Between,
FilterValue::Pair(
Box::new(a.into_filter_value()),
Box::new(b.into_filter_value()),
),
),
Lookup::Regex(s) => (LookupOp::Regex, FilterValue::String(s)),
Lookup::IRegex(s) => (LookupOp::IRegex, FilterValue::String(s)),
}
}
}
#[derive(Debug, Clone)]
pub struct FilterClause {
pub(crate) column: &'static str,
pub(crate) op: LookupOp,
pub(crate) value: FilterValue,
}
#[doc(hidden)]
pub struct FilterClauseParts {
pub column: &'static str,
pub op: LookupOp,
pub value: FilterValue,
}
impl FilterClauseParts {
#[doc(hidden)]
pub fn into_condition(self) -> Condition {
Condition::Leaf(Leaf::new(self.column, self.op, self.value))
}
}
impl FilterClause {
#[must_use]
pub fn from_lookup<V: IntoFilterValue>(column: &'static str, lookup: Lookup<V>) -> Self {
let (op, value) = lookup.into_op_value();
Self { column, op, value }
}
pub fn into_condition(self) -> Condition {
Condition::Leaf(Leaf::new(self.column, self.op, self.value))
}
#[doc(hidden)]
pub fn into_parts(self) -> FilterClauseParts {
FilterClauseParts {
column: self.column,
op: self.op,
value: self.value,
}
}
}
pub trait ModelFilter {
fn into_clauses(self) -> Vec<FilterClause>;
}
pub fn clauses_into_condition(clauses: Vec<FilterClause>) -> Condition {
match clauses.len() {
0 => Condition::True,
1 => {
clauses
.into_iter()
.next()
.expect("len == 1 branch guarantees one element")
.into_condition()
}
_ => Condition::And(
clauses
.into_iter()
.map(FilterClause::into_condition)
.collect(),
),
}
}
#[doc(hidden)]
pub fn clauses_into_q<M, F>(clauses: Vec<FilterClause>, mut map: F) -> Q<M>
where
M: Model,
F: FnMut(FilterClause) -> Q<M>,
{
match clauses.len() {
0 => Q::always_true(),
1 => {
let clause = clauses
.into_iter()
.next()
.expect("len == 1 branch guarantees one element");
map(clause)
}
_ => Q::Compound {
op: CompoundOp::And,
parts: clauses.into_iter().map(map).collect(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_eq_projects_to_eq_op_and_bool_value() {
let clause = FilterClause::from_lookup("published", Lookup::Eq(true));
assert_eq!(clause.column, "published");
assert_eq!(clause.op, LookupOp::Eq);
assert!(matches!(clause.value, FilterValue::Bool(true)));
}
#[test]
fn filter_clause_from_lookup_composes() {
let c = FilterClause::from_lookup("view_count", Lookup::Eq(42i32));
assert_eq!(c.column, "view_count");
assert_eq!(c.op, LookupOp::Eq);
assert!(matches!(c.value, FilterValue::I32(42)));
let c = FilterClause::from_lookup("id", Lookup::In(vec![1i64, 2, 3]));
assert_eq!(c.op, LookupOp::In);
assert!(matches!(c.value, FilterValue::List(ref v) if v.len() == 3));
let c = FilterClause::from_lookup("id", Lookup::<i64>::NotIn(Vec::new()));
assert_eq!(c.op, LookupOp::NotIn);
assert!(matches!(c.value, FilterValue::List(ref v) if v.is_empty()));
let c = FilterClause::from_lookup("age", Lookup::Between(10i32, 20i32));
assert_eq!(c.op, LookupOp::Between);
assert!(matches!(c.value, FilterValue::Pair(_, _)));
let c = FilterClause::from_lookup("deleted_at", Lookup::<String>::IsNull);
assert_eq!(c.op, LookupOp::IsNull);
assert!(matches!(c.value, FilterValue::Null));
let c = FilterClause::from_lookup("deleted_at", Lookup::<String>::IsNotNull);
assert_eq!(c.op, LookupOp::IsNotNull);
assert!(matches!(c.value, FilterValue::Null));
let c = FilterClause::from_lookup("title", Lookup::<String>::Contains("x".to_string()));
assert_eq!(c.op, LookupOp::IContains);
assert!(matches!(c.value, FilterValue::String(ref s) if s == "x"));
}
#[test]
fn lookup_gte_projects_to_gte_op_and_i32_value() {
let clause = FilterClause::from_lookup("view_count", Lookup::Gte(50i32));
assert_eq!(clause.op, LookupOp::Gte);
assert!(matches!(clause.value, FilterValue::I32(50)));
}
#[test]
fn lookup_in_builds_list_filter_value() {
let clause = FilterClause::from_lookup("id", Lookup::In(vec![1i64, 2, 3]));
assert_eq!(clause.op, LookupOp::In);
if let FilterValue::List(items) = clause.value {
assert_eq!(items.len(), 3);
} else {
panic!("expected FilterValue::List");
}
}
#[test]
fn lookup_between_builds_pair_filter_value() {
let clause = FilterClause::from_lookup("age", Lookup::Between(10i32, 20i32));
assert_eq!(clause.op, LookupOp::Between);
assert!(matches!(clause.value, FilterValue::Pair(_, _)));
}
#[test]
fn lookup_is_null_carries_null_filter_value() {
let clause = FilterClause::from_lookup("deleted_at", Lookup::<String>::IsNull);
assert_eq!(clause.op, LookupOp::IsNull);
assert!(matches!(clause.value, FilterValue::Null));
}
#[test]
fn lookup_contains_maps_to_icontains_op() {
let clause =
FilterClause::from_lookup("title", Lookup::<String>::Contains("hi".to_string()));
assert_eq!(clause.op, LookupOp::IContains);
assert!(matches!(clause.value, FilterValue::String(ref s) if s == "hi"));
}
#[test]
fn lookup_regex_maps_to_case_sensitive_regex_op() {
let clause = FilterClause::from_lookup("slug", Lookup::<String>::Regex("^foo".to_string()));
assert_eq!(clause.op, LookupOp::Regex);
}
#[test]
fn lookup_iregex_projects_to_iregex_op_and_string_value() {
let (op, value) = Lookup::<String>::IRegex("^foo".to_string()).into_op_value();
assert_eq!(op, LookupOp::IRegex);
assert!(matches!(value, FilterValue::String(ref s) if s == "^foo"));
let clause =
FilterClause::from_lookup("slug", Lookup::<String>::IRegex("^foo".to_string()));
assert_eq!(clause.column, "slug");
assert_eq!(clause.op, LookupOp::IRegex);
assert!(matches!(clause.value, FilterValue::String(ref s) if s == "^foo"));
}
#[test]
fn into_condition_produces_leaf() {
let clause = FilterClause::from_lookup("title", Lookup::Eq("x".to_string()));
let cond = clause.into_condition();
assert!(matches!(cond, Condition::Leaf(_)));
}
#[test]
fn clauses_into_condition_empty_is_true() {
let c = clauses_into_condition(Vec::new());
assert!(matches!(c, Condition::True));
}
#[test]
fn clauses_into_condition_single_unwraps_to_leaf() {
let clause = FilterClause::from_lookup("published", Lookup::Eq(true));
let c = clauses_into_condition(vec![clause]);
assert!(matches!(c, Condition::Leaf(_)));
}
#[test]
fn clauses_into_condition_many_builds_flat_and() {
let a = FilterClause::from_lookup("published", Lookup::Eq(true));
let b = FilterClause::from_lookup("view_count", Lookup::Gte(50i32));
let c = FilterClause::from_lookup("title", Lookup::Neq("draft".to_string()));
let cond = clauses_into_condition(vec![a, b, c]);
if let Condition::And(parts) = cond {
assert_eq!(parts.len(), 3);
for p in &parts {
assert!(matches!(p, Condition::Leaf(_)));
}
} else {
panic!("expected Condition::And with 3 elements");
}
}
struct FakeFilter {
clauses: Vec<FilterClause>,
}
impl ModelFilter for FakeFilter {
fn into_clauses(self) -> Vec<FilterClause> {
self.clauses
}
}
#[test]
fn model_filter_trait_returns_pushed_clauses() {
let f = FakeFilter {
clauses: vec![
FilterClause::from_lookup("a", Lookup::Eq(1i32)),
FilterClause::from_lookup("b", Lookup::Eq(2i32)),
],
};
let v = f.into_clauses();
assert_eq!(v.len(), 2);
assert_eq!(v[0].column, "a");
assert_eq!(v[1].column, "b");
}
}