use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::cmp::Ordering;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum FilterOp {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
In,
Nin,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum MetadataFilter {
Field {
key: String,
op: FilterOp,
value: Value,
},
And(Vec<MetadataFilter>),
Or(Vec<MetadataFilter>),
}
impl MetadataFilter {
pub fn field(key: impl Into<String>, op: FilterOp, value: impl Into<Value>) -> Self {
Self::Field {
key: key.into(),
op,
value: value.into(),
}
}
pub fn and(filters: Vec<MetadataFilter>) -> Self {
Self::And(filters)
}
pub fn or(filters: Vec<MetadataFilter>) -> Self {
Self::Or(filters)
}
pub fn matches(&self, metadata: &HashMap<String, Value>) -> bool {
match self {
Self::Field { key, op, value } => {
let Some(actual) = metadata.get(key) else {
return matches!(op, FilterOp::Ne | FilterOp::Nin);
};
Self::value_matches(op, actual, value)
}
Self::And(filters) => filters.iter().all(|f| f.matches(metadata)),
Self::Or(filters) => filters.iter().any(|f| f.matches(metadata)),
}
}
fn value_matches(op: &FilterOp, actual: &Value, expected: &Value) -> bool {
match op {
FilterOp::Eq => values_eq(actual, expected),
FilterOp::Ne => !values_eq(actual, expected),
FilterOp::Gt => values_cmp(actual, expected).is_some_and(|o| o == Ordering::Greater),
FilterOp::Gte => values_cmp(actual, expected).is_some_and(|o| o != Ordering::Less),
FilterOp::Lt => values_cmp(actual, expected).is_some_and(|o| o == Ordering::Less),
FilterOp::Lte => values_cmp(actual, expected).is_some_and(|o| o != Ordering::Greater),
FilterOp::In => expected
.as_array()
.is_some_and(|set| set.iter().any(|v| values_eq(actual, v))),
FilterOp::Nin => expected
.as_array()
.is_some_and(|set| !set.iter().any(|v| values_eq(actual, v))),
}
}
}
impl<'de> Deserialize<'de> for FilterOp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
let op = raw.trim().to_ascii_lowercase();
let parsed = match op.as_str() {
"eq" | "=" | "==" => FilterOp::Eq,
"ne" | "!=" | "<>" => FilterOp::Ne,
"gt" | ">" => FilterOp::Gt,
"gte" | ">=" => FilterOp::Gte,
"lt" | "<" => FilterOp::Lt,
"lte" | "<=" => FilterOp::Lte,
"in" => FilterOp::In,
"nin" | "not in" => FilterOp::Nin,
other => {
return Err(serde::de::Error::custom(format!(
"unknown metadata filter op: {other}"
)))
}
};
Ok(parsed)
}
}
impl<'de> Deserialize<'de> for MetadataFilter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
Self::from_json(value).map_err(serde::de::Error::custom)
}
}
impl MetadataFilter {
pub fn from_json(value: Value) -> Result<Self, String> {
let obj = value
.as_object()
.ok_or_else(|| format!("MetadataFilter must be an object, got {value}"))?;
if let Some(field) = obj.get("Field") {
return Self::field_from_json(field);
}
if let Some(items) = obj.get("And") {
let arr = items
.as_array()
.ok_or_else(|| "MetadataFilter And must be an array".to_string())?;
let filters: Result<Vec<_>, _> = arr.iter().cloned().map(Self::from_json).collect();
return Ok(Self::And(filters?));
}
if let Some(items) = obj.get("Or") {
let arr = items
.as_array()
.ok_or_else(|| "MetadataFilter Or must be an array".to_string())?;
let filters: Result<Vec<_>, _> = arr.iter().cloned().map(Self::from_json).collect();
return Ok(Self::Or(filters?));
}
if obj.contains_key("key") || obj.contains_key("op") {
return Self::field_from_json(&value);
}
Err(format!("unrecognized MetadataFilter JSON: {value}"))
}
fn field_from_json(value: &Value) -> Result<Self, String> {
let obj = value
.as_object()
.ok_or_else(|| format!("MetadataFilter field must be an object, got {value}"))?;
let key = obj
.get("key")
.and_then(|k| k.as_str())
.ok_or_else(|| "MetadataFilter field is missing string key".to_string())?
.to_string();
let op = serde_json::from_value(
obj.get("op")
.cloned()
.ok_or_else(|| "MetadataFilter field is missing op".to_string())?,
)
.map_err(|e| format!("invalid field op: {e}"))?;
let value = obj
.get("value")
.cloned()
.ok_or_else(|| "MetadataFilter field is missing value".to_string())?;
Ok(Self::Field { key, op, value })
}
}
fn values_eq(a: &Value, b: &Value) -> bool {
match (a.as_f64(), b.as_f64()) {
(Some(x), Some(y)) => x == y,
_ => a == b,
}
}
fn values_cmp(a: &Value, b: &Value) -> Option<Ordering> {
if let (Some(x), Some(y)) = (a.as_f64(), b.as_f64()) {
return x.partial_cmp(&y);
}
if let (Some(x), Some(y)) = (a.as_str(), b.as_str()) {
return Some(x.cmp(y));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn meta() -> HashMap<String, Value> {
let mut m = HashMap::new();
m.insert("source".to_string(), json!("docs"));
m.insert("year".to_string(), json!(2024));
m.insert("tags".to_string(), json!(["rust", "ml"]));
m
}
#[test]
fn test_eq_ne() {
let m = meta();
assert!(MetadataFilter::field("source", FilterOp::Eq, "docs").matches(&m));
assert!(!MetadataFilter::field("source", FilterOp::Eq, "blog").matches(&m));
assert!(MetadataFilter::field("source", FilterOp::Ne, "blog").matches(&m));
assert!(!MetadataFilter::field("source", FilterOp::Ne, "docs").matches(&m));
assert!(MetadataFilter::field("year", FilterOp::Eq, 2024.0).matches(&m));
}
#[test]
fn test_ordering_ops() {
let m = meta();
assert!(MetadataFilter::field("year", FilterOp::Gt, 2020).matches(&m));
assert!(MetadataFilter::field("year", FilterOp::Gte, 2024).matches(&m));
assert!(!MetadataFilter::field("year", FilterOp::Gt, 2024).matches(&m));
assert!(MetadataFilter::field("year", FilterOp::Lt, 2030).matches(&m));
assert!(MetadataFilter::field("year", FilterOp::Lte, 2024).matches(&m));
assert!(!MetadataFilter::field("year", FilterOp::Gt, "abc").matches(&m));
}
#[test]
fn test_in_nin() {
let m = meta();
assert!(MetadataFilter::field("source", FilterOp::In, vec!["docs", "web"]).matches(&m));
assert!(!MetadataFilter::field("source", FilterOp::In, vec!["blog", "web"]).matches(&m));
assert!(MetadataFilter::field("source", FilterOp::Nin, vec!["blog", "web"]).matches(&m));
assert!(!MetadataFilter::field("source", FilterOp::Nin, vec!["docs"]).matches(&m));
assert!(!MetadataFilter::field("source", FilterOp::In, "docs").matches(&m));
}
#[test]
fn test_and_or_composition() {
let m = meta();
let both = MetadataFilter::and(vec![
MetadataFilter::field("source", FilterOp::Eq, "docs"),
MetadataFilter::field("year", FilterOp::Gte, 2024),
]);
assert!(both.matches(&m));
let either = MetadataFilter::or(vec![
MetadataFilter::field("source", FilterOp::Eq, "blog"),
MetadataFilter::field("year", FilterOp::Gt, 2020),
]);
assert!(either.matches(&m));
let neither = MetadataFilter::or(vec![
MetadataFilter::field("source", FilterOp::Eq, "blog"),
MetadataFilter::field("year", FilterOp::Lt, 2000),
]);
assert!(!neither.matches(&m));
}
#[test]
fn test_missing_key_semantics() {
let m = meta();
assert!(!MetadataFilter::field("missing", FilterOp::Eq, "x").matches(&m));
assert!(!MetadataFilter::field("missing", FilterOp::In, vec!["x"]).matches(&m));
assert!(!MetadataFilter::field("missing", FilterOp::Gt, 1).matches(&m));
assert!(MetadataFilter::field("missing", FilterOp::Ne, "x").matches(&m));
assert!(MetadataFilter::field("missing", FilterOp::Nin, vec!["x"]).matches(&m));
}
#[test]
fn test_serialize_roundtrip() {
let f = MetadataFilter::and(vec![MetadataFilter::field("source", FilterOp::Eq, "docs")]);
let json = serde_json::to_string(&f).unwrap();
let back: MetadataFilter = serde_json::from_str(&json).unwrap();
assert_eq!(f, back);
}
#[test]
fn test_deserialize_lenient_shapes() {
let f: MetadataFilter =
serde_json::from_value(json!({"key": "source", "op": "eq", "value": "docs"})).unwrap();
assert_eq!(f, MetadataFilter::field("source", FilterOp::Eq, "docs"));
let f: MetadataFilter =
serde_json::from_value(json!({"key": "year", "op": ">=", "value": 2020})).unwrap();
assert_eq!(f, MetadataFilter::field("year", FilterOp::Gte, 2020));
let f: MetadataFilter = serde_json::from_value(json!({
"And": [
{"Field": {"key": "source", "op": "Eq", "value": "docs"}},
{"Field": {"key": "year", "op": "Lt", "value": 2030}}
]
}))
.unwrap();
assert_eq!(
f,
MetadataFilter::and(vec![
MetadataFilter::field("source", FilterOp::Eq, "docs"),
MetadataFilter::field("year", FilterOp::Lt, 2030),
])
);
let err = serde_json::from_value::<MetadataFilter>(
json!({"key": "year", "op": "like", "value": 2020}),
);
assert!(err.is_err());
}
}