#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ComparisonOp {
Eq,
Ne,
Gt,
Lt,
Gte,
Lte,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FieldValue {
Text(String),
Integer(i64),
Float(f64),
Boolean(bool),
Null,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FieldPath(String);
impl FieldPath {
#[must_use]
pub fn new(path: impl Into<String>) -> Self {
Self(path.into())
}
pub fn segments(&self) -> impl Iterator<Item = &str> + '_ {
self.0.split('.')
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Predicate {
Comparison {
field: FieldPath,
op: ComparisonOp,
value: FieldValue,
},
And(Vec<Self>),
Or(Vec<Self>),
Not(Box<Self>),
Range {
field: FieldPath,
lower: FieldValue,
upper: FieldValue,
},
Exists {
field: FieldPath,
},
}
#[cfg(test)]
mod tests {
use super::{ComparisonOp, FieldPath, FieldValue, Predicate};
#[test]
fn comparison_predicate_constructs() {
let predicate = Predicate::Comparison {
field: FieldPath::new("amount"),
op: ComparisonOp::Gt,
value: FieldValue::Integer(1_000),
};
assert_eq!(
predicate,
Predicate::Comparison {
field: FieldPath::new("amount"),
op: ComparisonOp::Gt,
value: FieldValue::Integer(1_000),
}
);
}
#[test]
fn comparison_debug_round_trips_all_variants() {
assert_eq!(format!("{:?}", ComparisonOp::Eq), "Eq");
assert_eq!(format!("{:?}", ComparisonOp::Ne), "Ne");
assert_eq!(format!("{:?}", ComparisonOp::Gt), "Gt");
assert_eq!(format!("{:?}", ComparisonOp::Lt), "Lt");
assert_eq!(format!("{:?}", ComparisonOp::Gte), "Gte");
assert_eq!(format!("{:?}", ComparisonOp::Lte), "Lte");
}
#[test]
fn boolean_combinators_construct() {
let amount = Predicate::Comparison {
field: FieldPath::new("amount"),
op: ComparisonOp::Gt,
value: FieldValue::Integer(1_000),
};
let region = Predicate::Comparison {
field: FieldPath::new("region"),
op: ComparisonOp::Eq,
value: FieldValue::Text(String::from("eu")),
};
let and = Predicate::And(vec![amount.clone(), region.clone()]);
let or = Predicate::Or(vec![amount.clone(), region.clone()]);
let not = Predicate::Not(Box::new(region.clone()));
let nested = Predicate::And(vec![amount.clone(), Predicate::Or(vec![region.clone()])]);
assert_eq!(and, Predicate::And(vec![amount.clone(), region.clone()]));
assert_eq!(or, Predicate::Or(vec![amount.clone(), region.clone()]));
assert_eq!(not, Predicate::Not(Box::new(region.clone())));
assert_eq!(
nested,
Predicate::And(vec![amount, Predicate::Or(vec![region])])
);
}
#[test]
fn empty_boolean_combinators_construct() {
assert_eq!(Predicate::And(Vec::new()), Predicate::And(Vec::new()));
assert_eq!(Predicate::Or(Vec::new()), Predicate::Or(Vec::new()));
}
#[test]
fn range_and_exists_predicates_construct() {
let integer_range = Predicate::Range {
field: FieldPath::new("amount"),
lower: FieldValue::Integer(100),
upper: FieldValue::Integer(200),
};
let text_range = Predicate::Range {
field: FieldPath::new("name"),
lower: FieldValue::Text(String::from("a")),
upper: FieldValue::Text(String::from("z")),
};
let exists = Predicate::Exists {
field: FieldPath::new("region"),
};
assert_eq!(
integer_range,
Predicate::Range {
field: FieldPath::new("amount"),
lower: FieldValue::Integer(100),
upper: FieldValue::Integer(200),
}
);
assert_eq!(
text_range,
Predicate::Range {
field: FieldPath::new("name"),
lower: FieldValue::Text(String::from("a")),
upper: FieldValue::Text(String::from("z")),
}
);
assert_eq!(
exists,
Predicate::Exists {
field: FieldPath::new("region"),
}
);
}
#[test]
fn field_path_segments_are_borrowed_dot_parts() {
let nested = FieldPath::new("user.address.city");
let nested_segments: Vec<_> = nested.segments().collect();
let single = FieldPath::new("name");
let single_segments: Vec<_> = single.segments().collect();
assert_eq!(nested_segments, ["user", "address", "city"]);
assert_eq!(single_segments, ["name"]);
}
#[test]
fn routing_root_re_exports_predicate_types() {
use crate::routing::{
ComparisonOp as RootComparisonOp, FieldPath as RootFieldPath,
FieldValue as RootFieldValue, Predicate as RootPredicate,
};
let predicate = RootPredicate::Comparison {
field: RootFieldPath::new("amount"),
op: RootComparisonOp::Gte,
value: RootFieldValue::Integer(100),
};
assert_eq!(
predicate,
RootPredicate::Comparison {
field: RootFieldPath::new("amount"),
op: RootComparisonOp::Gte,
value: RootFieldValue::Integer(100),
}
);
}
}