1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::status::{Error, Result};
6use crate::timestamp::Timestamp;
7
8pub type Metadata = BTreeMap<String, MetadataValue>;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum MetadataValue {
13 Bool(bool),
14 I64(i64),
15 F64(f64),
16 String(String),
17}
18
19impl From<bool> for MetadataValue {
20 fn from(value: bool) -> Self {
21 Self::Bool(value)
22 }
23}
24
25impl From<i64> for MetadataValue {
26 fn from(value: i64) -> Self {
27 Self::I64(value)
28 }
29}
30
31impl From<f64> for MetadataValue {
32 fn from(value: f64) -> Self {
33 Self::F64(value)
34 }
35}
36
37impl From<String> for MetadataValue {
38 fn from(value: String) -> Self {
39 Self::String(value)
40 }
41}
42
43impl From<&str> for MetadataValue {
44 fn from(value: &str) -> Self {
45 Self::String(value.to_string())
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum ComparisonOp {
52 Eq,
53 Ne,
54 Gt,
55 Gte,
56 Lt,
57 Lte,
58 Exists,
59 Contains,
60}
61
62#[derive(Debug, Clone, PartialEq, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct MetadataCondition {
65 pub metadata: String,
66 pub op: ComparisonOp,
67 #[serde(default)]
68 pub value: Option<MetadataValue>,
69}
70
71#[derive(Debug, Clone, PartialEq, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct TimestampComparison {
74 pub op: ComparisonOp,
75 #[serde(default)]
76 pub value: Option<i64>,
77}
78
79#[derive(Debug, Clone, PartialEq, Deserialize)]
80#[serde(untagged)]
81pub enum MetadataPredicate {
82 All { all: Vec<MetadataPredicate> },
83 Any { any: Vec<MetadataPredicate> },
84 Not { not: Box<MetadataPredicate> },
85 Timestamp { timestamp: TimestampComparison },
86 Metadata(MetadataCondition),
87}
88
89impl MetadataPredicate {
90 pub fn summary(&self) -> String {
91 match self {
92 Self::All { all } => format!(
93 "all({})",
94 all.iter().map(Self::summary).collect::<Vec<_>>().join(", ")
95 ),
96 Self::Any { any } => format!(
97 "any({})",
98 any.iter().map(Self::summary).collect::<Vec<_>>().join(", ")
99 ),
100 Self::Not { not } => format!("not({})", not.summary()),
101 Self::Timestamp { timestamp } => format!(
102 "timestamp {} {}",
103 op_name(timestamp.op),
104 timestamp
105 .value
106 .map_or_else(|| "?".into(), |value| value.to_string())
107 ),
108 Self::Metadata(condition) => {
109 let value = condition
110 .value
111 .as_ref()
112 .map_or_else(String::new, |value| format!(" {}", value_summary(value)));
113 format!("{} {}{}", condition.metadata, op_name(condition.op), value)
114 }
115 }
116 }
117
118 pub(crate) fn missing_metadata_count(&self, metadata: &Metadata) -> u64 {
119 match self {
120 Self::All { all } => all
121 .iter()
122 .map(|predicate| predicate.missing_metadata_count(metadata))
123 .sum(),
124 Self::Any { any } => any
125 .iter()
126 .map(|predicate| predicate.missing_metadata_count(metadata))
127 .sum(),
128 Self::Not { not } => not.missing_metadata_count(metadata),
129 Self::Timestamp { .. } => 0,
130 Self::Metadata(condition) => u64::from(!metadata.contains_key(&condition.metadata)),
131 }
132 }
133
134 pub fn validate(&self) -> Result<()> {
135 match self {
136 Self::All { all } => validate_group("all", all),
137 Self::Any { any } => validate_group("any", any),
138 Self::Not { not } => not.validate(),
139 Self::Timestamp { timestamp } => {
140 if matches!(timestamp.op, ComparisonOp::Exists | ComparisonOp::Contains) {
141 return Err(Error::InvalidArg(format!(
142 "timestamp condition does not support `{}`",
143 op_name(timestamp.op)
144 )));
145 }
146 if timestamp.value.is_none() {
147 return Err(Error::InvalidArg(
148 "timestamp condition requires `value`".into(),
149 ));
150 }
151 Ok(())
152 }
153 Self::Metadata(condition) => {
154 if condition.metadata.is_empty() {
155 return Err(Error::InvalidArg(
156 "metadata condition key must not be empty".into(),
157 ));
158 }
159 if condition.op == ComparisonOp::Exists {
160 if condition.value.is_some() {
161 return Err(Error::InvalidArg(
162 "metadata `exists` condition must not declare `value`".into(),
163 ));
164 }
165 } else if condition.value.is_none() {
166 return Err(Error::InvalidArg(format!(
167 "metadata `{}` condition requires `value`",
168 op_name(condition.op)
169 )));
170 }
171 if condition.op == ComparisonOp::Contains
172 && !matches!(condition.value, Some(MetadataValue::String(_)))
173 {
174 return Err(Error::InvalidArg(
175 "metadata `contains` condition requires a string value".into(),
176 ));
177 }
178 Ok(())
179 }
180 }
181 }
182
183 pub fn evaluate(&self, metadata: &Metadata, timestamp: Timestamp) -> Result<bool> {
184 match self {
185 Self::All { all } => {
186 for predicate in all {
187 if !predicate.evaluate(metadata, timestamp)? {
188 return Ok(false);
189 }
190 }
191 Ok(true)
192 }
193 Self::Any { any } => {
194 for predicate in any {
195 if predicate.evaluate(metadata, timestamp)? {
196 return Ok(true);
197 }
198 }
199 Ok(false)
200 }
201 Self::Not { not } => Ok(!not.evaluate(metadata, timestamp)?),
202 Self::Timestamp {
203 timestamp: condition,
204 } => compare_i64(
205 timestamp.0,
206 condition.op,
207 condition.value.expect("validated timestamp condition"),
208 ),
209 Self::Metadata(condition) => {
210 let actual = metadata.get(&condition.metadata);
211 if condition.op == ComparisonOp::Exists {
212 return Ok(actual.is_some());
213 }
214 let Some(actual) = actual else {
215 return Ok(false);
216 };
217 compare_metadata(
218 actual,
219 condition.op,
220 condition
221 .value
222 .as_ref()
223 .expect("validated metadata condition"),
224 )
225 }
226 }
227 }
228}
229
230fn validate_group(name: &str, predicates: &[MetadataPredicate]) -> Result<()> {
231 if predicates.is_empty() {
232 return Err(Error::InvalidArg(format!(
233 "metadata `{name}` condition must not be empty"
234 )));
235 }
236 predicates.iter().try_for_each(MetadataPredicate::validate)
237}
238
239fn compare_metadata(
240 actual: &MetadataValue,
241 op: ComparisonOp,
242 expected: &MetadataValue,
243) -> Result<bool> {
244 match (actual, expected) {
245 (MetadataValue::Bool(left), MetadataValue::Bool(right)) => compare_ord(left, op, right),
246 (MetadataValue::I64(left), MetadataValue::I64(right)) => compare_ord(left, op, right),
247 (MetadataValue::F64(left), MetadataValue::F64(right)) => compare_float(*left, op, *right),
248 (MetadataValue::I64(left), MetadataValue::F64(right)) => {
249 compare_float(*left as f64, op, *right)
250 }
251 (MetadataValue::F64(left), MetadataValue::I64(right)) => {
252 compare_float(*left, op, *right as f64)
253 }
254 (MetadataValue::String(left), MetadataValue::String(right)) => {
255 if op == ComparisonOp::Contains {
256 Ok(left.contains(right))
257 } else {
258 compare_ord(left, op, right)
259 }
260 }
261 _ => Err(Error::InvalidArg(format!(
262 "metadata comparison type mismatch for `{}`",
263 op_name(op)
264 ))),
265 }
266}
267
268fn compare_i64(left: i64, op: ComparisonOp, right: i64) -> Result<bool> {
269 compare_ord(&left, op, &right)
270}
271
272fn compare_float(left: f64, op: ComparisonOp, right: f64) -> Result<bool> {
273 Ok(match op {
274 ComparisonOp::Eq => left == right,
275 ComparisonOp::Ne => left != right,
276 ComparisonOp::Gt => left > right,
277 ComparisonOp::Gte => left >= right,
278 ComparisonOp::Lt => left < right,
279 ComparisonOp::Lte => left <= right,
280 ComparisonOp::Exists | ComparisonOp::Contains => {
281 return Err(Error::InvalidArg(format!(
282 "numeric comparison does not support `{}`",
283 op_name(op)
284 )));
285 }
286 })
287}
288
289fn compare_ord<T: PartialOrd + PartialEq>(left: &T, op: ComparisonOp, right: &T) -> Result<bool> {
290 Ok(match op {
291 ComparisonOp::Eq => left == right,
292 ComparisonOp::Ne => left != right,
293 ComparisonOp::Gt => left > right,
294 ComparisonOp::Gte => left >= right,
295 ComparisonOp::Lt => left < right,
296 ComparisonOp::Lte => left <= right,
297 ComparisonOp::Exists | ComparisonOp::Contains => {
298 return Err(Error::InvalidArg(format!(
299 "comparison does not support `{}`",
300 op_name(op)
301 )));
302 }
303 })
304}
305
306fn op_name(op: ComparisonOp) -> &'static str {
307 match op {
308 ComparisonOp::Eq => "eq",
309 ComparisonOp::Ne => "ne",
310 ComparisonOp::Gt => "gt",
311 ComparisonOp::Gte => "gte",
312 ComparisonOp::Lt => "lt",
313 ComparisonOp::Lte => "lte",
314 ComparisonOp::Exists => "exists",
315 ComparisonOp::Contains => "contains",
316 }
317}
318
319fn value_summary(value: &MetadataValue) -> String {
320 match value {
321 MetadataValue::Bool(value) => value.to_string(),
322 MetadataValue::I64(value) => value.to_string(),
323 MetadataValue::F64(value) => value.to_string(),
324 MetadataValue::String(value) => format!("\"{value}\""),
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn nested_predicate_evaluates() {
334 let predicate: MetadataPredicate = serde_yaml::from_str(
335 r#"
336all:
337 - { metadata: confidence, op: gte, value: 0.8 }
338 - any:
339 - { metadata: category, op: eq, value: person }
340 - timestamp: { op: gte, value: 100 }
341"#,
342 )
343 .unwrap();
344 predicate.validate().unwrap();
345 let metadata = Metadata::from([
346 ("confidence".into(), MetadataValue::F64(0.9)),
347 ("category".into(), MetadataValue::String("vehicle".into())),
348 ]);
349 assert!(predicate.evaluate(&metadata, Timestamp(100)).unwrap());
350 }
351}