1use serde_json::Value;
19
20#[derive(Debug, Clone, Default)]
23pub struct FilterSet {
24 patterns: Vec<Value>,
25}
26
27impl FilterSet {
28 pub fn from_strings<I, S>(raw: I) -> Self
34 where
35 I: IntoIterator<Item = S>,
36 S: AsRef<str>,
37 {
38 let patterns = raw
39 .into_iter()
40 .filter_map(|s| match serde_json::from_str::<Value>(s.as_ref()) {
41 Ok(v) => Some(v),
42 Err(err) => {
43 tracing::warn!(
44 pattern = s.as_ref(),
45 error = %err,
46 "lambda ESM filter pattern is invalid JSON; ignoring this pattern (other patterns still apply)"
47 );
48 None
49 }
50 })
51 .collect();
52 Self { patterns }
53 }
54
55 pub fn validate<I, S>(raw: I) -> Result<(), String>
60 where
61 I: IntoIterator<Item = S>,
62 S: AsRef<str>,
63 {
64 for s in raw {
65 serde_json::from_str::<Value>(s.as_ref())
66 .map_err(|err| format!("FilterCriteria pattern is invalid JSON: {err}"))?;
67 }
68 Ok(())
69 }
70
71 pub fn matches(&self, record: &Value) -> bool {
74 if self.patterns.is_empty() {
75 return true;
76 }
77 self.patterns.iter().any(|p| match_value(p, record))
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.patterns.is_empty()
82 }
83}
84
85fn match_value(pattern: &Value, value: &Value) -> bool {
86 eval_field(pattern, Some(value))
88}
89
90fn eval_field(pattern: &Value, value: Option<&Value>) -> bool {
95 if let Value::Object(po) = pattern {
96 if is_operator_object(po) {
97 return apply_operator(po, value);
98 }
99 }
100 match pattern {
101 Value::Object(po) => match value {
102 Some(Value::Object(vo)) => po.iter().all(|(k, sub_pattern)| {
103 if k == "body" {
104 if let Some(Value::String(s)) = vo.get("body") {
105 if let Ok(parsed) = serde_json::from_str::<Value>(s) {
106 return eval_field(sub_pattern, Some(&parsed));
107 }
108 }
109 }
110 eval_field(sub_pattern, vo.get(k))
111 }),
112 _ => false,
113 },
114 Value::Array(arr) => arr.iter().any(|p| eval_field(p, value)),
115 scalar => match (scalar, value) {
116 (Value::Null, Some(Value::Null)) => true,
117 (Value::Bool(a), Some(Value::Bool(b))) => a == b,
118 (Value::Number(a), Some(Value::Number(b))) => a == b,
119 (Value::String(a), Some(Value::String(b))) => a == b,
120 _ => false,
121 },
122 }
123}
124
125const OPERATOR_KEYS: &[&str] = &[
126 "exists",
127 "prefix",
128 "suffix",
129 "equals-ignore-case",
130 "anything-but",
131 "numeric",
132];
133
134fn is_operator_object(o: &serde_json::Map<String, Value>) -> bool {
135 o.keys().any(|k| OPERATOR_KEYS.contains(&k.as_str()))
136}
137
138fn apply_operator(o: &serde_json::Map<String, Value>, value: Option<&Value>) -> bool {
139 o.iter().all(|(op, arg)| match op.as_str() {
140 "exists" => matches!(
144 (arg, value),
145 (Value::Bool(true), Some(_)) | (Value::Bool(false), None)
146 ),
147 op_name => match value {
148 Some(v) => apply_value_operator(op_name, arg, v),
149 None => false,
150 },
151 })
152}
153
154fn apply_value_operator(op: &str, arg: &Value, value: &Value) -> bool {
155 match op {
156 "prefix" => match (arg.as_str(), value.as_str()) {
157 (Some(p), Some(s)) => s.starts_with(p),
158 _ => false,
159 },
160 "suffix" => match (arg.as_str(), value.as_str()) {
161 (Some(p), Some(s)) => s.ends_with(p),
162 _ => false,
163 },
164 "equals-ignore-case" => match (arg.as_str(), value.as_str()) {
165 (Some(p), Some(s)) => p.eq_ignore_ascii_case(s),
166 _ => false,
167 },
168 "anything-but" => match arg {
169 Value::Array(arr) => !arr.iter().any(|v| v == value),
170 other => other != value,
171 },
172 "numeric" => apply_numeric(arg, value),
173 _ => false,
174 }
175}
176
177fn apply_numeric(arg: &Value, value: &Value) -> bool {
178 let Some(n) = value.as_f64() else {
179 return false;
180 };
181 let Some(arr) = arg.as_array() else {
182 return false;
183 };
184 if arr.len() % 2 != 0 || arr.is_empty() {
188 return false;
189 }
190 for chunk in arr.chunks(2) {
191 let Some(target) = chunk[1].as_f64() else {
192 return false;
193 };
194 let ok = match chunk[0].as_str() {
195 Some("=") => n == target,
196 Some("<") => n < target,
197 Some("<=") => n <= target,
198 Some(">") => n > target,
199 Some(">=") => n >= target,
200 _ => false,
201 };
202 if !ok {
203 return false;
204 }
205 }
206 true
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use serde_json::json;
213
214 fn fs(patterns: &[&str]) -> FilterSet {
215 FilterSet::from_strings(patterns.iter().map(|s| s.to_string()))
216 }
217
218 #[test]
219 fn empty_pattern_passes_through() {
220 let f = FilterSet::default();
221 assert!(f.matches(&json!({"any": "thing"})));
222 }
223
224 #[test]
225 fn exact_string_match() {
226 let f = fs(&[r#"{"foo": "bar"}"#]);
227 assert!(f.matches(&json!({"foo": "bar"})));
228 assert!(!f.matches(&json!({"foo": "baz"})));
229 }
230
231 #[test]
232 fn array_of_scalars_is_or() {
233 let f = fs(&[r#"{"foo": ["a", "b"]}"#]);
234 assert!(f.matches(&json!({"foo": "a"})));
235 assert!(f.matches(&json!({"foo": "b"})));
236 assert!(!f.matches(&json!({"foo": "c"})));
237 }
238
239 #[test]
240 fn exists_operator_treats_null_as_present() {
241 let exists_true = fs(&[r#"{"foo": [{"exists": true}]}"#]);
242 assert!(exists_true.matches(&json!({"foo": null})));
244 let exists_false = fs(&[r#"{"foo": [{"exists": false}]}"#]);
245 assert!(exists_false.matches(&json!({})));
249 assert!(!exists_false.matches(&json!({"foo": null})));
250 }
251
252 #[test]
253 fn numeric_odd_length_is_no_match() {
254 let f = fs(&[r#"{"n": [{"numeric": [">", 0, "<"]}]}"#]);
255 assert!(!f.matches(&json!({"n": 5})));
256 }
257
258 #[test]
259 fn validate_rejects_invalid_json() {
260 assert!(FilterSet::validate(["{not json"].iter()).is_err());
261 assert!(FilterSet::validate([r#"{"ok": true}"#].iter()).is_ok());
262 }
263
264 #[test]
265 fn exists_operator() {
266 let exists_true = fs(&[r#"{"foo": [{"exists": true}]}"#]);
267 assert!(exists_true.matches(&json!({"foo": "x"})));
268 assert!(!exists_true.matches(&json!({"bar": "x"})));
269
270 let exists_false = fs(&[r#"{"foo": [{"exists": false}]}"#]);
271 assert!(!exists_false.matches(&json!({"foo": "x"})));
272 assert!(exists_false.matches(&json!({"bar": "x"})));
273 }
274
275 #[test]
276 fn sqs_body_decode() {
277 let f = fs(&[r#"{"body": {"action": "process"}}"#]);
278 let record = json!({
279 "body": "{\"action\": \"process\", \"id\": 42}",
280 });
281 assert!(f.matches(&record));
282 let other = json!({
283 "body": "{\"action\": \"skip\"}",
284 });
285 assert!(!f.matches(&other));
286 }
287
288 #[test]
289 fn nested_object_match() {
290 let f = fs(&[r#"{"order": {"status": "paid"}}"#]);
291 assert!(f.matches(&json!({"order": {"status": "paid", "id": 1}})));
292 assert!(!f.matches(&json!({"order": {"status": "pending"}})));
293 }
294
295 #[test]
296 fn multiple_patterns_or() {
297 let f = fs(&[r#"{"a": "x"}"#, r#"{"b": "y"}"#]);
298 assert!(f.matches(&json!({"a": "x"})));
299 assert!(f.matches(&json!({"b": "y"})));
300 assert!(!f.matches(&json!({"c": "z"})));
301 }
302}