use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EqCondition {
pub path: String,
pub value: JsonValue,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Filter {
And {
#[serde(rename = "$and")]
filters: Vec<Filter>,
},
Or {
#[serde(rename = "$or")]
filters: Vec<Filter>,
},
Not {
#[serde(rename = "$not")]
filter: Box<Filter>,
},
EqWrapped {
#[serde(rename = "$eq")]
condition: EqCondition,
},
Eq {
path: String,
value: JsonValue,
},
}
#[derive(Debug, Clone)]
pub struct CompiledSegment {
field: String,
idx: Option<usize>,
}
impl CompiledSegment {
fn from_str(s: &str) -> Self {
Self {
field: s.to_string(),
idx: s.parse().ok(),
}
}
}
#[derive(Debug, Clone)]
pub enum CompiledFilter {
And(Vec<CompiledFilter>),
Or(Vec<CompiledFilter>),
Not(Box<CompiledFilter>),
Eq {
segments: Vec<CompiledSegment>,
value: JsonValue,
},
}
impl CompiledFilter {
#[inline]
pub fn matches(&self, event: &JsonValue) -> bool {
match self {
Self::And(filters) if filters.len() == 1 => filters[0].matches(event),
Self::Or(filters) if filters.len() == 1 => filters[0].matches(event),
Self::And(filters) => !filters.is_empty() && filters.iter().all(|f| f.matches(event)),
Self::Or(filters) => filters.iter().any(|f| f.matches(event)),
Self::Not(f) => !f.matches(event),
Self::Eq { segments, value } => json_path_get_compiled(event, segments) == Some(value),
}
}
}
#[inline]
fn json_path_get_compiled<'a>(
value: &'a JsonValue,
segments: &[CompiledSegment],
) -> Option<&'a JsonValue> {
if segments.is_empty() {
return Some(value);
}
let mut current = value;
for seg in segments {
current = match current {
JsonValue::Object(map) => map.get(&seg.field)?,
JsonValue::Array(arr) => arr.get(seg.idx?)?,
_ => return None,
};
}
Some(current)
}
impl Filter {
pub fn and(filters: Vec<Filter>) -> Self {
Self::And { filters }
}
pub fn or(filters: Vec<Filter>) -> Self {
Self::Or { filters }
}
#[allow(clippy::should_implement_trait)]
pub fn not(filter: Filter) -> Self {
Self::Not {
filter: Box::new(filter),
}
}
pub fn eq(path: impl Into<String>, value: JsonValue) -> Self {
Self::Eq {
path: path.into(),
value,
}
}
#[inline]
pub fn matches(&self, event: &JsonValue) -> bool {
match self {
Self::And { filters } if filters.len() == 1 => filters[0].matches(event),
Self::Or { filters } if filters.len() == 1 => filters[0].matches(event),
Self::And { filters } => {
!filters.is_empty() && filters.iter().all(|f| f.matches(event))
}
Self::Or { filters } => filters.iter().any(|f| f.matches(event)),
Self::Not { filter } => !filter.matches(event),
Self::EqWrapped { condition } => {
json_path_get(event, &condition.path) == Some(&condition.value)
}
Self::Eq { path, value } => json_path_get(event, path) == Some(value),
}
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn compile(&self) -> CompiledFilter {
match self {
Self::And { filters } => {
CompiledFilter::And(filters.iter().map(Self::compile).collect())
}
Self::Or { filters } => CompiledFilter::Or(filters.iter().map(Self::compile).collect()),
Self::Not { filter } => CompiledFilter::Not(Box::new(filter.compile())),
Self::Eq { path, value } => CompiledFilter::Eq {
segments: compile_path(path),
value: value.clone(),
},
Self::EqWrapped { condition } => CompiledFilter::Eq {
segments: compile_path(&condition.path),
value: condition.value.clone(),
},
}
}
}
fn compile_path(path: &str) -> Vec<CompiledSegment> {
if path.is_empty() {
Vec::new()
} else {
path.split('.').map(CompiledSegment::from_str).collect()
}
}
#[inline]
pub fn json_path_get<'a>(value: &'a JsonValue, path: &str) -> Option<&'a JsonValue> {
if path.is_empty() {
return Some(value);
}
let mut current = value;
for segment in path.split('.') {
current = match current {
JsonValue::Object(map) => map.get(segment)?,
JsonValue::Array(arr) => {
let idx: usize = segment.parse().ok()?;
arr.get(idx)?
}
_ => return None,
};
}
Some(current)
}
#[derive(Debug, Default)]
pub struct FilterBuilder {
filters: Vec<Filter>,
}
impl FilterBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn eq(mut self, path: impl Into<String>, value: JsonValue) -> Self {
self.filters.push(Filter::eq(path, value));
self
}
#[expect(
clippy::unwrap_used,
reason = "len == 1 branch guarantees the iterator yields exactly one element"
)]
pub fn build_and(self) -> Filter {
if self.filters.len() == 1 {
self.filters.into_iter().next().unwrap()
} else {
Filter::and(self.filters)
}
}
#[expect(
clippy::unwrap_used,
reason = "len == 1 branch guarantees the iterator yields exactly one element"
)]
pub fn build_or(self) -> Filter {
if self.filters.len() == 1 {
self.filters.into_iter().next().unwrap()
} else {
Filter::or(self.filters)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_eq_filter() {
let filter = Filter::eq("type", json!("token"));
assert!(filter.matches(&json!({"type": "token", "value": "hello"})));
assert!(!filter.matches(&json!({"type": "message", "value": "hello"})));
assert!(!filter.matches(&json!({"value": "hello"}))); }
#[test]
fn from_json_rejects_adversarially_nested_filter() {
let depth = 10_000usize;
let mut json = String::with_capacity(depth * 8 + 32);
for _ in 0..depth {
json.push_str(r#"{"$not":"#);
}
json.push_str(r#"{"path":"x","value":1}"#);
for _ in 0..depth {
json.push('}');
}
let parsed = Filter::from_json(&json);
assert!(
parsed.is_err(),
"depth-{depth} filter JSON must be rejected by serde_json's recursion limit"
);
}
#[test]
fn matches_handles_modest_depth_on_small_stack() {
const DEPTH: usize = 256;
let mut f = Filter::eq("x", json!(1));
for _ in 0..DEPTH {
f = Filter::not(f);
}
let result = std::thread::Builder::new()
.stack_size(256 * 1024)
.spawn(move || f.matches(&json!({"x": 1})))
.expect("spawn small-stack thread")
.join()
.expect("matches() must not panic at depth 256 on a small stack");
assert!(result, "depth-256 nested Not over true Eq should be true");
}
#[test]
fn test_nested_path() {
let filter = Filter::eq("user.profile.name", json!("Alice"));
assert!(filter.matches(&json!({
"user": {
"profile": {
"name": "Alice",
"age": 30
}
}
})));
assert!(!filter.matches(&json!({
"user": {
"profile": {
"name": "Bob"
}
}
})));
}
#[test]
fn test_array_indexing() {
let filter = Filter::eq("items.0.name", json!("first"));
assert!(filter.matches(&json!({
"items": [
{"name": "first"},
{"name": "second"}
]
})));
assert!(!filter.matches(&json!({
"items": [
{"name": "other"}
]
})));
}
#[test]
fn test_and_filter() {
let filter = Filter::and(vec![
Filter::eq("type", json!("token")),
Filter::eq("index", json!(0)),
]);
assert!(filter.matches(&json!({"type": "token", "index": 0})));
assert!(!filter.matches(&json!({"type": "token", "index": 1})));
assert!(!filter.matches(&json!({"type": "message", "index": 0})));
}
#[test]
fn test_or_filter() {
let filter = Filter::or(vec![
Filter::eq("type", json!("token")),
Filter::eq("type", json!("message")),
]);
assert!(filter.matches(&json!({"type": "token"})));
assert!(filter.matches(&json!({"type": "message"})));
assert!(!filter.matches(&json!({"type": "error"})));
}
#[test]
fn test_not_filter() {
let filter = Filter::not(Filter::eq("type", json!("error")));
assert!(filter.matches(&json!({"type": "token"})));
assert!(filter.matches(&json!({"type": "message"})));
assert!(!filter.matches(&json!({"type": "error"})));
}
#[test]
fn test_complex_filter() {
let filter = Filter::and(vec![
Filter::eq("type", json!("token")),
Filter::or(vec![
Filter::eq("value", json!("hello")),
Filter::eq("value", json!("world")),
]),
Filter::not(Filter::eq("user", json!("bot"))),
]);
assert!(filter.matches(&json!({
"type": "token",
"value": "hello",
"user": "alice"
})));
assert!(!filter.matches(&json!({
"type": "token",
"value": "hello",
"user": "bot" })));
assert!(!filter.matches(&json!({
"type": "token",
"value": "other", "user": "alice"
})));
}
#[test]
fn test_filter_builder() {
let filter = FilterBuilder::new()
.eq("type", json!("token"))
.eq("active", json!(true))
.build_and();
assert!(filter.matches(&json!({"type": "token", "active": true})));
assert!(!filter.matches(&json!({"type": "token", "active": false})));
}
#[test]
fn test_filter_serialization() {
let filter = Filter::and(vec![
Filter::eq("type", json!("token")),
Filter::not(Filter::eq("error", json!(true))),
]);
let json = filter.to_json().unwrap();
let parsed: Filter = Filter::from_json(&json).unwrap();
let event = json!({"type": "token", "error": false});
assert_eq!(filter.matches(&event), parsed.matches(&event));
}
#[test]
fn compiled_filter_matches_raw_filter_semantically() {
let raw: Filter = serde_json::from_str(
r#"{"$and": [
{"path": "user.profile.name", "value": "Alice"},
{"$or": [
{"path": "items.0", "value": "first"},
{"$eq": {"path": "items.1", "value": "second"}}
]},
{"$not": {"path": "user.profile.role", "value": "guest"}}
]}"#,
)
.unwrap();
let compiled = raw.compile();
let events = [
serde_json::json!({
"user": {"profile": {"name": "Alice", "role": "admin"}},
"items": ["first", "second"]
}),
serde_json::json!({
"user": {"profile": {"name": "Bob", "role": "admin"}},
"items": ["first", "second"]
}),
serde_json::json!({
"user": {"profile": {"name": "Alice", "role": "guest"}},
"items": ["first", "second"]
}),
serde_json::json!({
"user": {"profile": {"name": "Alice", "role": "admin"}}
}),
serde_json::json!({
"user": {"profile": {"name": "Alice", "role": "admin"}},
"items": ["first"]
}),
];
for ev in &events {
assert_eq!(
compiled.matches(ev),
raw.matches(ev),
"compiled vs raw diverge on {ev:?}",
);
}
}
#[test]
fn compile_caches_array_index_parse_per_segment() {
let f = Filter::eq("items.42.foo", serde_json::json!(1));
let compiled = f.compile();
let CompiledFilter::Eq { segments, .. } = compiled else {
panic!("expected CompiledFilter::Eq");
};
assert_eq!(segments.len(), 3);
assert_eq!(segments[0].field, "items");
assert!(
segments[0].idx.is_none(),
"'items' must not pre-parse as usize",
);
assert_eq!(segments[1].field, "42");
assert_eq!(
segments[1].idx,
Some(42),
"'42' must pre-parse as Some(42) — cached integer index",
);
assert_eq!(segments[2].field, "foo");
assert!(segments[2].idx.is_none());
}
#[test]
fn test_json_path_get() {
let value = json!({
"a": {
"b": {
"c": 42
}
},
"arr": [1, 2, 3],
"nested_arr": [{"x": 10}, {"x": 20}]
});
assert_eq!(json_path_get(&value, "a.b.c"), Some(&json!(42)));
assert_eq!(json_path_get(&value, "arr.1"), Some(&json!(2)));
assert_eq!(json_path_get(&value, "nested_arr.0.x"), Some(&json!(10)));
assert_eq!(json_path_get(&value, "missing"), None);
assert_eq!(json_path_get(&value, "a.b.missing"), None);
assert_eq!(json_path_get(&value, ""), Some(&value));
}
#[test]
fn test_json_path_get_primitive() {
let value = json!(42);
assert_eq!(json_path_get(&value, "foo"), None);
let value = json!("string");
assert_eq!(json_path_get(&value, "bar"), None);
let value = json!(true);
assert_eq!(json_path_get(&value, "baz"), None);
let value = json!(null);
assert_eq!(json_path_get(&value, "qux"), None);
}
#[test]
fn test_json_path_get_invalid_array_index() {
let value = json!({"arr": [1, 2, 3]});
assert_eq!(json_path_get(&value, "arr.foo"), None);
assert_eq!(json_path_get(&value, "arr.100"), None);
}
#[test]
fn test_filter_builder_single() {
let filter = FilterBuilder::new().eq("type", json!("token")).build_and();
assert!(matches!(filter, Filter::Eq { .. }));
let filter = FilterBuilder::new().eq("type", json!("token")).build_or();
assert!(matches!(filter, Filter::Eq { .. }));
}
#[test]
fn test_filter_builder_multiple_or() {
let filter = FilterBuilder::new()
.eq("type", json!("a"))
.eq("type", json!("b"))
.build_or();
assert!(filter.matches(&json!({"type": "a"})));
assert!(filter.matches(&json!({"type": "b"})));
assert!(!filter.matches(&json!({"type": "c"})));
}
#[test]
fn test_filter_clone() {
let filter = Filter::and(vec![
Filter::eq("a", json!(1)),
Filter::not(Filter::eq("b", json!(2))),
]);
let cloned = filter.clone();
let event = json!({"a": 1, "b": 3});
assert_eq!(filter.matches(&event), cloned.matches(&event));
}
#[test]
fn test_filter_debug() {
let filter = Filter::eq("type", json!("token"));
let debug = format!("{:?}", filter);
assert!(debug.contains("Eq"));
assert!(debug.contains("type"));
}
#[test]
fn test_filter_partial_eq() {
let f1 = Filter::eq("type", json!("token"));
let f2 = Filter::eq("type", json!("token"));
let f3 = Filter::eq("type", json!("other"));
assert_eq!(f1, f2);
assert_ne!(f1, f3);
}
#[test]
fn test_empty_and_filter() {
let filter = Filter::and(vec![]);
assert!(
!filter.matches(&json!({"any": "value"})),
"empty And must not match — was silently universal-pass before"
);
}
#[test]
fn test_empty_or_filter() {
let filter = Filter::or(vec![]);
assert!(!filter.matches(&json!({"any": "value"})));
}
#[test]
fn test_single_element_and_or_match_inner_filter() {
let inner = Filter::eq("k", json!("v"));
let single_and = Filter::and(vec![inner.clone()]);
let single_or = Filter::or(vec![inner.clone()]);
let yes = json!({"k": "v"});
let no = json!({"k": "other"});
for ev in &[yes, no] {
assert_eq!(
single_and.matches(ev),
inner.matches(ev),
"single-element And must match inner: {ev}",
);
assert_eq!(
single_or.matches(ev),
inner.matches(ev),
"single-element Or must match inner: {ev}",
);
}
}
#[test]
fn test_single_element_fast_path_recurses_into_composite() {
let leaf = Filter::eq("k", json!("v"));
let yes = json!({"k": "v"});
let no = json!({"k": "other"});
let nested_not = Filter::and(vec![Filter::not(leaf.clone())]);
assert!(!nested_not.matches(&yes));
assert!(nested_not.matches(&no));
let nested_double = Filter::or(vec![Filter::and(vec![leaf.clone()])]);
assert!(nested_double.matches(&yes));
assert!(!nested_double.matches(&no));
let leaf2 = Filter::eq("x", json!(1));
let mixed = Filter::or(vec![Filter::and(vec![leaf.clone(), leaf2.clone()])]);
assert!(mixed.matches(&json!({"k": "v", "x": 1})));
assert!(!mixed.matches(&json!({"k": "v", "x": 2})));
assert!(!mixed.matches(&json!({"k": "other", "x": 1})));
}
#[test]
fn test_multi_element_and_or_uses_slow_path() {
let f1 = Filter::eq("k", json!("v"));
let f2 = Filter::eq("x", json!(1));
let and = Filter::and(vec![f1.clone(), f2.clone()]);
assert!(and.matches(&json!({"k": "v", "x": 1})));
assert!(!and.matches(&json!({"k": "v", "x": 2})));
assert!(!and.matches(&json!({"k": "other", "x": 1})));
let or = Filter::or(vec![f1.clone(), f2.clone()]);
assert!(or.matches(&json!({"k": "v", "x": 99})));
assert!(or.matches(&json!({"k": "nope", "x": 1})));
assert!(!or.matches(&json!({"k": "nope", "x": 2})));
}
#[test]
fn test_filter_builder_default() {
let builder = FilterBuilder::default();
let debug = format!("{:?}", builder);
assert!(debug.contains("FilterBuilder"));
}
#[test]
fn test_eq_wrapped_filter_deserialization() {
let json_str = r#"{"$eq": {"path": "type", "value": "token"}}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"type": "token", "data": "hello"})));
assert!(!filter.matches(&json!({"type": "message", "data": "hello"})));
}
#[test]
fn test_eq_wrapped_with_nested_path() {
let json_str = r#"{"$eq": {"path": "user.role", "value": "admin"}}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"user": {"role": "admin"}})));
assert!(!filter.matches(&json!({"user": {"role": "user"}})));
}
#[test]
fn test_eq_wrapped_with_numeric_value() {
let json_str = r#"{"$eq": {"path": "count", "value": 42}}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"count": 42})));
assert!(!filter.matches(&json!({"count": 41})));
}
#[test]
fn test_eq_wrapped_with_boolean_value() {
let json_str = r#"{"$eq": {"path": "active", "value": true}}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"active": true})));
assert!(!filter.matches(&json!({"active": false})));
}
#[test]
fn test_eq_wrapped_in_and() {
let json_str = r#"{"$and": [{"$eq": {"path": "type", "value": "token"}}, {"$eq": {"path": "index", "value": 0}}]}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"type": "token", "index": 0})));
assert!(!filter.matches(&json!({"type": "token", "index": 1})));
assert!(!filter.matches(&json!({"type": "message", "index": 0})));
}
#[test]
fn test_eq_wrapped_in_or() {
let json_str = r#"{"$or": [{"$eq": {"path": "type", "value": "token"}}, {"$eq": {"path": "type", "value": "message"}}]}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"type": "token"})));
assert!(filter.matches(&json!({"type": "message"})));
assert!(!filter.matches(&json!({"type": "error"})));
}
#[test]
fn test_eq_wrapped_in_not() {
let json_str = r#"{"$not": {"$eq": {"path": "type", "value": "error"}}}"#;
let filter: Filter = serde_json::from_str(json_str).unwrap();
assert!(filter.matches(&json!({"type": "token"})));
assert!(filter.matches(&json!({"type": "message"})));
assert!(!filter.matches(&json!({"type": "error"})));
}
#[test]
fn test_both_eq_formats_work() {
let shorthand = r#"{"path": "type", "value": "token"}"#;
let wrapped = r#"{"$eq": {"path": "type", "value": "token"}}"#;
let filter1: Filter = serde_json::from_str(shorthand).unwrap();
let filter2: Filter = serde_json::from_str(wrapped).unwrap();
let event = json!({"type": "token"});
assert!(filter1.matches(&event));
assert!(filter2.matches(&event));
let event2 = json!({"type": "other"});
assert!(!filter1.matches(&event2));
assert!(!filter2.matches(&event2));
}
}