use std::fmt;
use std::ops::Not;
use super::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum Bound {
Literal(Value),
Field(String),
}
impl fmt::Display for Bound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Bound::Literal(v) => write!(f, "{v}"),
Bound::Field(path) => write!(f, "{path}"),
}
}
}
impl From<Value> for Bound {
fn from(v: Value) -> Self {
Bound::Literal(v)
}
}
impl From<i64> for Bound {
fn from(v: i64) -> Self {
Bound::Literal(Value::Int(v))
}
}
impl From<f64> for Bound {
fn from(v: f64) -> Self {
Bound::Literal(Value::Float(v))
}
}
impl From<bool> for Bound {
fn from(v: bool) -> Self {
Bound::Literal(Value::Bool(v))
}
}
impl From<&str> for Bound {
fn from(v: &str) -> Self {
Bound::Literal(Value::String(v.to_owned()))
}
}
impl From<String> for Bound {
fn from(v: String) -> Self {
Bound::Literal(Value::String(v))
}
}
#[must_use]
pub fn bound_field(path: &str) -> Bound {
Bound::Field(path.to_owned())
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum CompiledBound {
Literal(Value),
FieldIndex(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Neq,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Compare {
field: String,
op: CompareOp,
value: Value,
},
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
Not(Box<Expr>),
RuleRef(String),
In {
field: String,
members: Vec<Bound>,
},
NotIn {
field: String,
members: Vec<Bound>,
},
Between {
field: String,
low: Bound,
high: Bound,
},
Like {
field: String,
pattern: String,
},
NotLike {
field: String,
pattern: String,
},
IsNull(String),
IsNotNull(String),
CompareFields {
left: String,
op: CompareOp,
right: String,
},
AtLeast {
n: usize,
exprs: Vec<Expr>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum CompiledExpr {
Compare {
field_index: usize,
op: CompareOp,
value: Value,
},
And(Box<CompiledExpr>, Box<CompiledExpr>),
Or(Box<CompiledExpr>, Box<CompiledExpr>),
Not(Box<CompiledExpr>),
RuleRef(usize),
In {
field_index: usize,
members: Vec<CompiledBound>,
},
NotIn {
field_index: usize,
members: Vec<CompiledBound>,
},
Between {
field_index: usize,
low: CompiledBound,
high: CompiledBound,
},
Like {
field_index: usize,
pattern: String,
},
NotLike {
field_index: usize,
pattern: String,
},
IsNull(usize),
IsNotNull(usize),
CompareFields {
left_index: usize,
op: CompareOp,
right_index: usize,
},
AtLeast {
n: usize,
exprs: Vec<CompiledExpr>,
},
}
impl fmt::Display for CompareOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompareOp::Eq => write!(f, "=="),
CompareOp::Neq => write!(f, "!="),
CompareOp::Gt => write!(f, ">"),
CompareOp::Gte => write!(f, ">="),
CompareOp::Lt => write!(f, "<"),
CompareOp::Lte => write!(f, "<="),
}
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expr::Compare { field, op, value } => write!(f, "({field} {op} {value})"),
Expr::And(a, b) => write!(f, "({a} AND {b})"),
Expr::Or(a, b) => write!(f, "({a} OR {b})"),
Expr::Not(inner) => write!(f, "(NOT {inner})"),
Expr::RuleRef(name) => write!(f, "{name}"),
Expr::In { field, members } => {
let vals: Vec<String> = members.iter().map(ToString::to_string).collect();
write!(f, "({field} IN [{}])", vals.join(", "))
}
Expr::NotIn { field, members } => {
let vals: Vec<String> = members.iter().map(ToString::to_string).collect();
write!(f, "({field} NOT IN [{}])", vals.join(", "))
}
Expr::Between { field, low, high } => {
write!(f, "({field} BETWEEN {low}, {high})")
}
Expr::Like { field, pattern } => write!(f, "({field} LIKE \"{pattern}\")"),
Expr::NotLike { field, pattern } => write!(f, "({field} NOT LIKE \"{pattern}\")"),
Expr::IsNull(field) => write!(f, "({field} IS NULL)"),
Expr::IsNotNull(field) => write!(f, "({field} IS NOT NULL)"),
Expr::CompareFields { left, op, right } => write!(f, "({left} {op} {right})"),
Expr::AtLeast { n, exprs } => {
let parts: Vec<String> = exprs.iter().map(ToString::to_string).collect();
write!(f, "AT_LEAST({n}, {})", parts.join(", "))
}
}
}
}
impl Expr {
#[must_use]
pub fn and(self, other: Expr) -> Expr {
Expr::And(Box::new(self), Box::new(other))
}
#[must_use]
pub fn or(self, other: Expr) -> Expr {
Expr::Or(Box::new(self), Box::new(other))
}
}
impl Not for Expr {
type Output = Expr;
fn not(self) -> Expr {
Expr::Not(Box::new(self))
}
}
#[derive(Debug, Clone)]
pub struct FieldExpr {
path: String,
}
impl FieldExpr {
#[must_use]
pub fn eq(self, value: impl Into<Value>) -> Expr {
Expr::Compare {
field: self.path,
op: CompareOp::Eq,
value: value.into(),
}
}
#[must_use]
pub fn neq(self, value: impl Into<Value>) -> Expr {
Expr::Compare {
field: self.path,
op: CompareOp::Neq,
value: value.into(),
}
}
#[must_use]
pub fn gt(self, value: impl Into<Value>) -> Expr {
Expr::Compare {
field: self.path,
op: CompareOp::Gt,
value: value.into(),
}
}
#[must_use]
pub fn gte(self, value: impl Into<Value>) -> Expr {
Expr::Compare {
field: self.path,
op: CompareOp::Gte,
value: value.into(),
}
}
#[must_use]
pub fn lt(self, value: impl Into<Value>) -> Expr {
Expr::Compare {
field: self.path,
op: CompareOp::Lt,
value: value.into(),
}
}
#[must_use]
pub fn lte(self, value: impl Into<Value>) -> Expr {
Expr::Compare {
field: self.path,
op: CompareOp::Lte,
value: value.into(),
}
}
#[must_use]
pub fn is_in<I, V>(self, values: I) -> Expr
where
I: IntoIterator<Item = V>,
V: Into<Bound>,
{
Expr::In {
field: self.path,
members: values.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn is_in_field(self, list_field: &str) -> Expr {
Expr::In {
field: self.path,
members: vec![Bound::Field(list_field.to_owned())],
}
}
#[must_use]
pub fn not_in<I, V>(self, values: I) -> Expr
where
I: IntoIterator<Item = V>,
V: Into<Bound>,
{
Expr::NotIn {
field: self.path,
members: values.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn between(self, low: impl Into<Bound>, high: impl Into<Bound>) -> Expr {
Expr::Between {
field: self.path,
low: low.into(),
high: high.into(),
}
}
#[must_use]
pub fn like(self, pattern: impl Into<String>) -> Expr {
Expr::Like {
field: self.path,
pattern: pattern.into(),
}
}
#[must_use]
pub fn not_like(self, pattern: impl Into<String>) -> Expr {
Expr::NotLike {
field: self.path,
pattern: pattern.into(),
}
}
#[must_use]
pub fn is_null(self) -> Expr {
Expr::IsNull(self.path)
}
#[must_use]
pub fn is_not_null(self) -> Expr {
Expr::IsNotNull(self.path)
}
#[must_use]
pub fn eq_field(self, right: &str) -> Expr {
Expr::CompareFields {
left: self.path,
op: CompareOp::Eq,
right: right.to_owned(),
}
}
#[must_use]
pub fn neq_field(self, right: &str) -> Expr {
Expr::CompareFields {
left: self.path,
op: CompareOp::Neq,
right: right.to_owned(),
}
}
#[must_use]
pub fn gt_field(self, right: &str) -> Expr {
Expr::CompareFields {
left: self.path,
op: CompareOp::Gt,
right: right.to_owned(),
}
}
#[must_use]
pub fn gte_field(self, right: &str) -> Expr {
Expr::CompareFields {
left: self.path,
op: CompareOp::Gte,
right: right.to_owned(),
}
}
#[must_use]
pub fn lt_field(self, right: &str) -> Expr {
Expr::CompareFields {
left: self.path,
op: CompareOp::Lt,
right: right.to_owned(),
}
}
#[must_use]
pub fn lte_field(self, right: &str) -> Expr {
Expr::CompareFields {
left: self.path,
op: CompareOp::Lte,
right: right.to_owned(),
}
}
}
#[must_use]
pub fn field(path: &str) -> FieldExpr {
FieldExpr {
path: path.to_owned(),
}
}
#[must_use]
pub fn rule_ref(name: &str) -> Expr {
Expr::RuleRef(name.to_owned())
}
#[must_use]
pub fn at_least(n: usize, exprs: impl IntoIterator<Item = Expr>) -> Expr {
Expr::AtLeast {
n,
exprs: exprs.into_iter().collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Value;
#[test]
fn field_eq_i64() {
let expr = field("user.age").eq(18_i64);
assert_eq!(
expr,
Expr::Compare {
field: "user.age".to_owned(),
op: CompareOp::Eq,
value: Value::Int(18),
}
);
}
#[test]
fn field_gte_with_into() {
let expr = field("score").gte(90_i64);
assert_eq!(
expr,
Expr::Compare {
field: "score".to_owned(),
op: CompareOp::Gte,
value: Value::Int(90),
}
);
}
#[test]
fn field_eq_str() {
let expr = field("status").eq("active");
assert_eq!(
expr,
Expr::Compare {
field: "status".to_owned(),
op: CompareOp::Eq,
value: Value::String("active".to_owned()),
}
);
}
#[test]
fn rule_ref_creates_expr() {
let expr = rule_ref("some_rule");
assert_eq!(expr, Expr::RuleRef("some_rule".to_owned()));
}
#[test]
fn and_chaining() {
let expr = rule_ref("a").and(rule_ref("b"));
assert_eq!(
expr,
Expr::And(
Box::new(Expr::RuleRef("a".to_owned())),
Box::new(Expr::RuleRef("b".to_owned())),
)
);
}
#[test]
fn or_chaining() {
let expr = field("x").eq(1_i64).or(field("y").eq(2_i64));
match expr {
Expr::Or(_, _) => {}
other => panic!("expected Or, got {other:?}"),
}
}
#[test]
fn not_expr() {
let expr = !field("banned").eq(true);
match expr {
Expr::Not(_) => {}
other => panic!("expected Not, got {other:?}"),
}
}
#[test]
fn complex_expression_tree() {
let expr = rule_ref("eligible_age")
.and(rule_ref("active_account"))
.and(rule_ref("not_restricted"));
match &expr {
Expr::And(left, right) => {
assert_eq!(**right, Expr::RuleRef("not_restricted".to_owned()));
match left.as_ref() {
Expr::And(ll, lr) => {
assert_eq!(**ll, Expr::RuleRef("eligible_age".to_owned()));
assert_eq!(**lr, Expr::RuleRef("active_account".to_owned()));
}
other => panic!("expected inner And, got {other:?}"),
}
}
other => panic!("expected outer And, got {other:?}"),
}
}
#[test]
fn field_is_in_literals() {
let expr = field("country").is_in(["US", "CA", "GB"]);
assert_eq!(
expr,
Expr::In {
field: "country".to_owned(),
members: vec![
Bound::Literal(Value::String("US".to_owned())),
Bound::Literal(Value::String("CA".to_owned())),
Bound::Literal(Value::String("GB".to_owned())),
],
}
);
}
#[test]
fn field_not_in_literals() {
let expr = field("status").not_in(["banned", "suspended"]);
assert_eq!(
expr,
Expr::NotIn {
field: "status".to_owned(),
members: vec![
Bound::Literal(Value::String("banned".to_owned())),
Bound::Literal(Value::String("suspended".to_owned())),
],
}
);
}
#[test]
fn field_is_in_with_field_ref() {
let expr = field("role").is_in([Bound::from("admin"), bound_field("team.default_role")]);
assert_eq!(
expr,
Expr::In {
field: "role".to_owned(),
members: vec![
Bound::Literal(Value::String("admin".to_owned())),
Bound::Field("team.default_role".to_owned()),
],
}
);
}
#[test]
fn field_between_literals() {
let expr = field("age").between(18_i64, 65_i64);
assert_eq!(
expr,
Expr::Between {
field: "age".to_owned(),
low: Bound::Literal(Value::Int(18)),
high: Bound::Literal(Value::Int(65)),
}
);
}
#[test]
fn field_between_field_bounds() {
let expr = field("score").between(bound_field("tier.min"), bound_field("tier.max"));
assert_eq!(
expr,
Expr::Between {
field: "score".to_owned(),
low: Bound::Field("tier.min".to_owned()),
high: Bound::Field("tier.max".to_owned()),
}
);
}
#[test]
fn field_between_mixed_bounds() {
let expr = field("score").between(10_i64, bound_field("tier.max_score"));
assert_eq!(
expr,
Expr::Between {
field: "score".to_owned(),
low: Bound::Literal(Value::Int(10)),
high: Bound::Field("tier.max_score".to_owned()),
}
);
}
#[test]
fn field_like() {
let expr = field("email").like("%@gmail.com");
assert_eq!(
expr,
Expr::Like {
field: "email".to_owned(),
pattern: "%@gmail.com".to_owned(),
}
);
}
#[test]
fn field_not_like() {
let expr = field("email").not_like("%@test.%");
assert_eq!(
expr,
Expr::NotLike {
field: "email".to_owned(),
pattern: "%@test.%".to_owned(),
}
);
}
#[test]
fn field_is_null() {
let expr = field("middle_name").is_null();
assert_eq!(expr, Expr::IsNull("middle_name".to_owned()));
}
#[test]
fn field_is_not_null() {
let expr = field("middle_name").is_not_null();
assert_eq!(expr, Expr::IsNotNull("middle_name".to_owned()));
}
#[test]
fn all_compare_ops() {
let ops = vec![
(field("f").eq(1_i64), CompareOp::Eq),
(field("f").neq(1_i64), CompareOp::Neq),
(field("f").gt(1_i64), CompareOp::Gt),
(field("f").gte(1_i64), CompareOp::Gte),
(field("f").lt(1_i64), CompareOp::Lt),
(field("f").lte(1_i64), CompareOp::Lte),
];
for (expr, expected_op) in ops {
match expr {
Expr::Compare { op, .. } => assert_eq!(op, expected_op),
other => panic!("expected Compare, got {other:?}"),
}
}
}
}