use serde_json::Value as JsonValue;
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
Eq {
field: String,
value: JsonValue,
},
Ne {
field: String,
value: JsonValue,
},
Gt {
field: String,
value: f64,
},
Gte {
field: String,
value: f64,
},
Lt {
field: String,
value: f64,
},
Lte {
field: String,
value: f64,
},
Range {
field: String,
min: Option<f64>,
max: Option<f64>,
},
In {
field: String,
values: Vec<JsonValue>,
},
NotIn {
field: String,
values: Vec<JsonValue>,
},
Contains {
field: String,
substring: String,
},
StartsWith {
field: String,
prefix: String,
},
ArrayContains {
field: String,
value: JsonValue,
},
Exists {
field: String,
},
NotExists {
field: String,
},
And(Vec<Filter>),
Or(Vec<Filter>),
Not(Box<Filter>),
}
impl Filter {
pub fn eq(field: impl Into<String>, value: impl Into<JsonValue>) -> Self {
Self::Eq { field: field.into(), value: value.into() }
}
pub fn ne(field: impl Into<String>, value: impl Into<JsonValue>) -> Self {
Self::Ne { field: field.into(), value: value.into() }
}
pub fn gt(field: impl Into<String>, value: impl Into<f64>) -> Self {
Self::Gt { field: field.into(), value: value.into() }
}
pub fn gte(field: impl Into<String>, value: impl Into<f64>) -> Self {
Self::Gte { field: field.into(), value: value.into() }
}
pub fn lt(field: impl Into<String>, value: impl Into<f64>) -> Self {
Self::Lt { field: field.into(), value: value.into() }
}
pub fn lte(field: impl Into<String>, value: impl Into<f64>) -> Self {
Self::Lte { field: field.into(), value: value.into() }
}
pub fn range(field: impl Into<String>, min: Option<f64>, max: Option<f64>) -> Self {
Self::Range { field: field.into(), min, max }
}
pub fn in_set<V: Into<JsonValue>>(
field: impl Into<String>,
values: impl IntoIterator<Item = V>,
) -> Self {
Self::In { field: field.into(), values: values.into_iter().map(Into::into).collect() }
}
pub fn not_in<V: Into<JsonValue>>(
field: impl Into<String>,
values: impl IntoIterator<Item = V>,
) -> Self {
Self::NotIn { field: field.into(), values: values.into_iter().map(Into::into).collect() }
}
pub fn contains(field: impl Into<String>, substring: impl Into<String>) -> Self {
Self::Contains { field: field.into(), substring: substring.into() }
}
pub fn starts_with(field: impl Into<String>, prefix: impl Into<String>) -> Self {
Self::StartsWith { field: field.into(), prefix: prefix.into() }
}
pub fn array_contains(field: impl Into<String>, value: impl Into<JsonValue>) -> Self {
Self::ArrayContains { field: field.into(), value: value.into() }
}
pub fn exists(field: impl Into<String>) -> Self {
Self::Exists { field: field.into() }
}
pub fn not_exists(field: impl Into<String>) -> Self {
Self::NotExists { field: field.into() }
}
pub fn and(filters: impl IntoIterator<Item = Filter>) -> Self {
Self::And(filters.into_iter().collect())
}
pub fn or(filters: impl IntoIterator<Item = Filter>) -> Self {
Self::Or(filters.into_iter().collect())
}
pub fn not(filter: Filter) -> Self {
Self::Not(Box::new(filter))
}
#[must_use]
pub fn and_then(self, other: Filter) -> Self {
match self {
Self::And(mut filters) => {
filters.push(other);
Self::And(filters)
}
_ => Self::And(vec![self, other]),
}
}
#[must_use]
pub fn or_else(self, other: Filter) -> Self {
match self {
Self::Or(mut filters) => {
filters.push(other);
Self::Or(filters)
}
_ => Self::Or(vec![self, other]),
}
}
#[must_use]
pub fn matches(&self, payload: &JsonValue) -> bool {
match self {
Self::Eq { field, value } => {
get_field(payload, field).map(|v| values_equal(v, value)).unwrap_or(false)
}
Self::Ne { field, value } => {
get_field(payload, field).map(|v| !values_equal(v, value)).unwrap_or(true)
}
Self::Gt { field, value } => get_field(payload, field)
.and_then(|v| v.as_f64())
.map(|v| v > *value)
.unwrap_or(false),
Self::Gte { field, value } => get_field(payload, field)
.and_then(|v| v.as_f64())
.map(|v| v >= *value)
.unwrap_or(false),
Self::Lt { field, value } => get_field(payload, field)
.and_then(|v| v.as_f64())
.map(|v| v < *value)
.unwrap_or(false),
Self::Lte { field, value } => get_field(payload, field)
.and_then(|v| v.as_f64())
.map(|v| v <= *value)
.unwrap_or(false),
Self::Range { field, min, max } => get_field(payload, field)
.and_then(|v| v.as_f64())
.map(|v| {
let above_min = min.map_or(true, |m| v >= m);
let below_max = max.map_or(true, |m| v <= m);
above_min && below_max
})
.unwrap_or(false),
Self::In { field, values } => get_field(payload, field)
.map(|v| values.iter().any(|val| values_equal(v, val)))
.unwrap_or(false),
Self::NotIn { field, values } => get_field(payload, field)
.map(|v| !values.iter().any(|val| values_equal(v, val)))
.unwrap_or(true),
Self::Contains { field, substring } => get_field(payload, field)
.and_then(|v| v.as_str())
.map(|s| s.contains(substring.as_str()))
.unwrap_or(false),
Self::StartsWith { field, prefix } => get_field(payload, field)
.and_then(|v| v.as_str())
.map(|s| s.starts_with(prefix.as_str()))
.unwrap_or(false),
Self::ArrayContains { field, value } => get_field(payload, field)
.and_then(|v| v.as_array())
.map(|arr| arr.iter().any(|item| values_equal(item, value)))
.unwrap_or(false),
Self::Exists { field } => {
get_field(payload, field).map(|v| !v.is_null()).unwrap_or(false)
}
Self::NotExists { field } => {
get_field(payload, field).map(|v| v.is_null()).unwrap_or(true)
}
Self::And(filters) => filters.iter().all(|f| f.matches(payload)),
Self::Or(filters) => filters.iter().any(|f| f.matches(payload)),
Self::Not(filter) => !filter.matches(payload),
}
}
#[must_use]
pub fn matches_entity(&self, entity: &manifoldb_core::Entity) -> bool {
let payload = entity_properties_to_json(entity);
self.matches(&payload)
}
}
fn entity_properties_to_json(entity: &manifoldb_core::Entity) -> JsonValue {
let mut map = serde_json::Map::new();
for (key, value) in &entity.properties {
map.insert(key.clone(), value_to_json(value));
}
JsonValue::Object(map)
}
fn value_to_json(value: &manifoldb_core::Value) -> JsonValue {
match value {
manifoldb_core::Value::Null => JsonValue::Null,
manifoldb_core::Value::Bool(b) => JsonValue::Bool(*b),
manifoldb_core::Value::Int(i) => JsonValue::Number((*i).into()),
manifoldb_core::Value::Float(f) => {
serde_json::Number::from_f64(*f).map_or(JsonValue::Null, JsonValue::Number)
}
manifoldb_core::Value::String(s) => JsonValue::String(s.clone()),
manifoldb_core::Value::Bytes(b) => {
use base64::Engine;
JsonValue::String(base64::engine::general_purpose::STANDARD.encode(b))
}
manifoldb_core::Value::Array(items) => {
JsonValue::Array(items.iter().map(value_to_json).collect())
}
manifoldb_core::Value::SparseVector(pairs) => {
JsonValue::Array(
pairs
.iter()
.map(|(idx, val)| {
JsonValue::Array(vec![
JsonValue::Number((*idx).into()),
serde_json::Number::from_f64(*val as f64)
.map_or(JsonValue::Null, JsonValue::Number),
])
})
.collect(),
)
}
manifoldb_core::Value::MultiVector(vecs) => {
JsonValue::Array(
vecs.iter()
.map(|v| {
JsonValue::Array(
v.iter()
.map(|f| {
serde_json::Number::from_f64(*f as f64)
.map_or(JsonValue::Null, JsonValue::Number)
})
.collect(),
)
})
.collect(),
)
}
manifoldb_core::Value::Vector(v) => JsonValue::Array(
v.iter()
.map(|f| {
JsonValue::Number(
serde_json::Number::from_f64(*f as f64)
.unwrap_or_else(|| serde_json::Number::from(0)),
)
})
.collect(),
),
}
}
fn get_field<'a>(payload: &'a JsonValue, field: &str) -> Option<&'a JsonValue> {
let mut current = payload;
for part in field.split('.') {
current = current.get(part)?;
}
Some(current)
}
fn values_equal(a: &JsonValue, b: &JsonValue) -> bool {
match (a, b) {
(JsonValue::Number(a), JsonValue::Number(b)) => {
match (a.as_f64(), b.as_f64()) {
(Some(a), Some(b)) => (a - b).abs() < f64::EPSILON,
_ => a == b,
}
}
_ => a == b,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_eq_filter() {
let filter = Filter::eq("category", "programming");
let payload = json!({"category": "programming", "title": "Rust Book"});
assert!(filter.matches(&payload));
let payload = json!({"category": "fiction"});
assert!(!filter.matches(&payload));
}
#[test]
fn test_numeric_filters() {
let payload = json!({"price": 25.0, "rating": 4.5});
assert!(Filter::gt("price", 20.0).matches(&payload));
assert!(!Filter::gt("price", 30.0).matches(&payload));
assert!(Filter::gte("price", 25.0).matches(&payload));
assert!(!Filter::gte("price", 26.0).matches(&payload));
assert!(Filter::lt("price", 30.0).matches(&payload));
assert!(!Filter::lt("price", 20.0).matches(&payload));
assert!(Filter::lte("price", 25.0).matches(&payload));
assert!(!Filter::lte("price", 24.0).matches(&payload));
}
#[test]
fn test_range_filter() {
let filter = Filter::range("price", Some(10.0), Some(50.0));
assert!(filter.matches(&json!({"price": 25.0})));
assert!(filter.matches(&json!({"price": 10.0})));
assert!(filter.matches(&json!({"price": 50.0})));
assert!(!filter.matches(&json!({"price": 5.0})));
assert!(!filter.matches(&json!({"price": 100.0})));
}
#[test]
fn test_in_filter() {
let filter = Filter::in_set("category", ["fiction", "poetry"]);
assert!(filter.matches(&json!({"category": "fiction"})));
assert!(filter.matches(&json!({"category": "poetry"})));
assert!(!filter.matches(&json!({"category": "programming"})));
}
#[test]
fn test_contains_filter() {
let filter = Filter::contains("title", "Rust");
assert!(filter.matches(&json!({"title": "The Rust Book"})));
assert!(!filter.matches(&json!({"title": "Python Guide"})));
}
#[test]
fn test_and_filter() {
let filter =
Filter::and([Filter::eq("category", "programming"), Filter::gte("rating", 4.0)]);
assert!(filter.matches(&json!({"category": "programming", "rating": 4.5})));
assert!(!filter.matches(&json!({"category": "programming", "rating": 3.5})));
assert!(!filter.matches(&json!({"category": "fiction", "rating": 4.5})));
}
#[test]
fn test_or_filter() {
let filter =
Filter::or([Filter::eq("category", "fiction"), Filter::eq("category", "poetry")]);
assert!(filter.matches(&json!({"category": "fiction"})));
assert!(filter.matches(&json!({"category": "poetry"})));
assert!(!filter.matches(&json!({"category": "programming"})));
}
#[test]
fn test_not_filter() {
let filter = Filter::not(Filter::eq("status", "deleted"));
assert!(filter.matches(&json!({"status": "active"})));
assert!(!filter.matches(&json!({"status": "deleted"})));
}
#[test]
fn test_nested_field() {
let filter = Filter::eq("metadata.author", "John");
let payload = json!({"metadata": {"author": "John", "year": 2024}});
assert!(filter.matches(&payload));
}
#[test]
fn test_array_contains() {
let filter = Filter::array_contains("tags", "rust");
assert!(filter.matches(&json!({"tags": ["rust", "programming"]})));
assert!(!filter.matches(&json!({"tags": ["python", "programming"]})));
}
#[test]
fn test_exists() {
let filter = Filter::exists("optional_field");
assert!(filter.matches(&json!({"optional_field": "value"})));
assert!(!filter.matches(&json!({"optional_field": null})));
assert!(!filter.matches(&json!({"other_field": "value"})));
}
#[test]
fn test_filter_chaining() {
let filter = Filter::eq("category", "programming")
.and_then(Filter::gte("rating", 4.0))
.and_then(Filter::lt("price", 50.0));
assert!(filter.matches(&json!({
"category": "programming",
"rating": 4.5,
"price": 30.0
})));
}
#[test]
fn test_matches_entity() {
use manifoldb_core::{Entity, EntityId};
let entity = Entity::new(EntityId::new(1))
.with_label("Test")
.with_property("language", "rust")
.with_property("rating", 4.5f64);
let filter = Filter::and([Filter::eq("language", "rust"), Filter::gte("rating", 4.0)]);
assert!(filter.matches_entity(&entity));
let filter2 = Filter::eq("language", "python");
assert!(!filter2.matches_entity(&entity));
}
}