use serde_json::Value;
#[derive(Debug, Clone, Default)]
pub struct FilterSet {
patterns: Vec<Value>,
}
impl FilterSet {
pub fn from_strings<I, S>(raw: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let patterns = raw
.into_iter()
.filter_map(|s| match serde_json::from_str::<Value>(s.as_ref()) {
Ok(v) => Some(v),
Err(err) => {
tracing::warn!(
pattern = s.as_ref(),
error = %err,
"lambda ESM filter pattern is invalid JSON; ignoring this pattern (other patterns still apply)"
);
None
}
})
.collect();
Self { patterns }
}
pub fn validate<I, S>(raw: I) -> Result<(), String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for s in raw {
serde_json::from_str::<Value>(s.as_ref())
.map_err(|err| format!("FilterCriteria pattern is invalid JSON: {err}"))?;
}
Ok(())
}
pub fn matches(&self, record: &Value) -> bool {
if self.patterns.is_empty() {
return true;
}
self.patterns.iter().any(|p| match_value(p, record))
}
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
}
fn match_value(pattern: &Value, value: &Value) -> bool {
eval_field(pattern, Some(value))
}
fn eval_field(pattern: &Value, value: Option<&Value>) -> bool {
if let Value::Object(po) = pattern {
if is_operator_object(po) {
return apply_operator(po, value);
}
}
match pattern {
Value::Object(po) => match value {
Some(Value::Object(vo)) => po.iter().all(|(k, sub_pattern)| {
if k == "body" {
if let Some(Value::String(s)) = vo.get("body") {
if let Ok(parsed) = serde_json::from_str::<Value>(s) {
return eval_field(sub_pattern, Some(&parsed));
}
}
}
eval_field(sub_pattern, vo.get(k))
}),
_ => false,
},
Value::Array(arr) => arr.iter().any(|p| eval_field(p, value)),
scalar => match (scalar, value) {
(Value::Null, Some(Value::Null)) => true,
(Value::Bool(a), Some(Value::Bool(b))) => a == b,
(Value::Number(a), Some(Value::Number(b))) => a == b,
(Value::String(a), Some(Value::String(b))) => a == b,
_ => false,
},
}
}
const OPERATOR_KEYS: &[&str] = &[
"exists",
"prefix",
"suffix",
"equals-ignore-case",
"anything-but",
"numeric",
];
fn is_operator_object(o: &serde_json::Map<String, Value>) -> bool {
o.keys().any(|k| OPERATOR_KEYS.contains(&k.as_str()))
}
fn apply_operator(o: &serde_json::Map<String, Value>, value: Option<&Value>) -> bool {
o.iter().all(|(op, arg)| match op.as_str() {
"exists" => matches!(
(arg, value),
(Value::Bool(true), Some(_)) | (Value::Bool(false), None)
),
op_name => match value {
Some(v) => apply_value_operator(op_name, arg, v),
None => false,
},
})
}
fn apply_value_operator(op: &str, arg: &Value, value: &Value) -> bool {
match op {
"prefix" => match (arg.as_str(), value.as_str()) {
(Some(p), Some(s)) => s.starts_with(p),
_ => false,
},
"suffix" => match (arg.as_str(), value.as_str()) {
(Some(p), Some(s)) => s.ends_with(p),
_ => false,
},
"equals-ignore-case" => match (arg.as_str(), value.as_str()) {
(Some(p), Some(s)) => p.eq_ignore_ascii_case(s),
_ => false,
},
"anything-but" => match arg {
Value::Array(arr) => !arr.iter().any(|v| v == value),
other => other != value,
},
"numeric" => apply_numeric(arg, value),
_ => false,
}
}
fn apply_numeric(arg: &Value, value: &Value) -> bool {
let Some(n) = value.as_f64() else {
return false;
};
let Some(arr) = arg.as_array() else {
return false;
};
if arr.len() % 2 != 0 || arr.is_empty() {
return false;
}
for chunk in arr.chunks(2) {
let Some(target) = chunk[1].as_f64() else {
return false;
};
let ok = match chunk[0].as_str() {
Some("=") => n == target,
Some("<") => n < target,
Some("<=") => n <= target,
Some(">") => n > target,
Some(">=") => n >= target,
_ => false,
};
if !ok {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn fs(patterns: &[&str]) -> FilterSet {
FilterSet::from_strings(patterns.iter().map(|s| s.to_string()))
}
#[test]
fn empty_pattern_passes_through() {
let f = FilterSet::default();
assert!(f.matches(&json!({"any": "thing"})));
}
#[test]
fn exact_string_match() {
let f = fs(&[r#"{"foo": "bar"}"#]);
assert!(f.matches(&json!({"foo": "bar"})));
assert!(!f.matches(&json!({"foo": "baz"})));
}
#[test]
fn array_of_scalars_is_or() {
let f = fs(&[r#"{"foo": ["a", "b"]}"#]);
assert!(f.matches(&json!({"foo": "a"})));
assert!(f.matches(&json!({"foo": "b"})));
assert!(!f.matches(&json!({"foo": "c"})));
}
#[test]
fn exists_operator_treats_null_as_present() {
let exists_true = fs(&[r#"{"foo": [{"exists": true}]}"#]);
assert!(exists_true.matches(&json!({"foo": null})));
let exists_false = fs(&[r#"{"foo": [{"exists": false}]}"#]);
assert!(exists_false.matches(&json!({})));
assert!(!exists_false.matches(&json!({"foo": null})));
}
#[test]
fn numeric_odd_length_is_no_match() {
let f = fs(&[r#"{"n": [{"numeric": [">", 0, "<"]}]}"#]);
assert!(!f.matches(&json!({"n": 5})));
}
#[test]
fn validate_rejects_invalid_json() {
assert!(FilterSet::validate(["{not json"].iter()).is_err());
assert!(FilterSet::validate([r#"{"ok": true}"#].iter()).is_ok());
}
#[test]
fn exists_operator() {
let exists_true = fs(&[r#"{"foo": [{"exists": true}]}"#]);
assert!(exists_true.matches(&json!({"foo": "x"})));
assert!(!exists_true.matches(&json!({"bar": "x"})));
let exists_false = fs(&[r#"{"foo": [{"exists": false}]}"#]);
assert!(!exists_false.matches(&json!({"foo": "x"})));
assert!(exists_false.matches(&json!({"bar": "x"})));
}
#[test]
fn sqs_body_decode() {
let f = fs(&[r#"{"body": {"action": "process"}}"#]);
let record = json!({
"body": "{\"action\": \"process\", \"id\": 42}",
});
assert!(f.matches(&record));
let other = json!({
"body": "{\"action\": \"skip\"}",
});
assert!(!f.matches(&other));
}
#[test]
fn nested_object_match() {
let f = fs(&[r#"{"order": {"status": "paid"}}"#]);
assert!(f.matches(&json!({"order": {"status": "paid", "id": 1}})));
assert!(!f.matches(&json!({"order": {"status": "pending"}})));
}
#[test]
fn multiple_patterns_or() {
let f = fs(&[r#"{"a": "x"}"#, r#"{"b": "y"}"#]);
assert!(f.matches(&json!({"a": "x"})));
assert!(f.matches(&json!({"b": "y"})));
assert!(!f.matches(&json!({"c": "z"})));
}
}