Skip to main content

jmap_types/
query.rs

1//! RFC 8620 §5.5 generic filter types for JMAP `/query` methods.
2//!
3//! Provides [`Filter`], [`FilterOperator`], and [`Operator`].
4//! Object-specific filter conditions (e.g. `EmailFilterCondition`) are
5//! defined in their respective type crates.
6
7use serde::{Deserialize, Serialize};
8
9/// Logical operator for combining filter conditions (RFC 8620 §5.5).
10#[non_exhaustive]
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
13pub enum Operator {
14    And,
15    Or,
16    Not,
17}
18
19/// A filter node: either a logical operator combining sub-filters, or a
20/// type-specific condition object (RFC 8620 §5.5).
21///
22/// Serializes as an untagged union.  The presence of the `"operator"` key
23/// distinguishes `Filter::Operator` from `Filter::Condition`.
24///
25/// **Variant ordering is critical**: `Operator` is listed before `Condition`
26/// because serde untagged tries variants in declaration order.
27/// `FilterOperator<T>` requires an `"operator"` field and fails fast without
28/// it, allowing the deserializer to fall through to `Condition(T)`.
29#[non_exhaustive]
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(untagged)]
32pub enum Filter<T> {
33    /// A logical combination of sub-filters.
34    Operator(FilterOperator<T>),
35    /// A type-specific condition object.
36    Condition(T),
37}
38
39/// Logical combination of filters (RFC 8620 §5.5).
40#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct FilterOperator<T> {
43    /// Logical operator: AND, OR, or NOT.
44    pub operator: Operator,
45    /// Sub-conditions to evaluate.
46    pub conditions: Vec<Filter<T>>,
47}
48
49impl<T> FilterOperator<T> {
50    /// Create a new [`FilterOperator`] with the given logical operator and conditions.
51    pub fn new(operator: Operator, conditions: Vec<Filter<T>>) -> Self {
52        Self {
53            operator,
54            conditions,
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    // FilterCondition stub used only within this test module.
64    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65    struct Cond {
66        #[serde(skip_serializing_if = "Option::is_none")]
67        pub has_keyword: Option<String>,
68    }
69
70    /// Oracle: exercises the Filter<T> generic with a local stub type.
71    /// Adapted from the RFC 8620 §5.5 Todo/query example; `has_keyword` uses
72    /// Rust snake_case because `Cond` is a test stub, not a real JMAP type.
73    #[test]
74    fn filter_operator_or_roundtrip() {
75        let json =
76            r#"{"operator":"OR","conditions":[{"has_keyword":"music"},{"has_keyword":"video"}]}"#;
77        let f: Filter<Cond> = serde_json::from_str(json).expect("must parse");
78        match &f {
79            Filter::Operator(op) => {
80                assert_eq!(op.operator, Operator::Or);
81                assert_eq!(op.conditions.len(), 2);
82            }
83            other => panic!("expected Operator, got {other:?}"),
84        }
85        let back = serde_json::to_string(&f).expect("must serialize");
86        let f2: Filter<Cond> = serde_json::from_str(&back).expect("roundtrip");
87        assert_eq!(f, f2);
88    }
89
90    /// Oracle: a bare condition object (no "operator" key) deserializes as
91    /// Filter::Condition.
92    #[test]
93    fn filter_condition_deserialization() {
94        let json = r#"{"has_keyword":"$seen"}"#;
95        let f: Filter<Cond> = serde_json::from_str(json).expect("must parse");
96        match &f {
97            Filter::Condition(c) => assert_eq!(c.has_keyword.as_deref(), Some("$seen")),
98            other => panic!("expected Condition, got {other:?}"),
99        }
100    }
101
102    /// Oracle: Operator enum serializes as SCREAMING_SNAKE_CASE per RFC 8620 §5.5.
103    #[test]
104    fn operator_serialization() {
105        assert_eq!(serde_json::to_string(&Operator::And).unwrap(), r#""AND""#);
106        assert_eq!(serde_json::to_string(&Operator::Or).unwrap(), r#""OR""#);
107        assert_eq!(serde_json::to_string(&Operator::Not).unwrap(), r#""NOT""#);
108    }
109
110    /// Oracle: nested AND(OR(...)) structure roundtrips correctly.
111    #[test]
112    fn nested_filter_roundtrip() {
113        let filter = Filter::Operator(FilterOperator {
114            operator: Operator::And,
115            conditions: vec![
116                Filter::Operator(FilterOperator {
117                    operator: Operator::Or,
118                    conditions: vec![
119                        Filter::Condition(Cond {
120                            has_keyword: Some("a".to_owned()),
121                        }),
122                        Filter::Condition(Cond {
123                            has_keyword: Some("b".to_owned()),
124                        }),
125                    ],
126                }),
127                Filter::Condition(Cond {
128                    has_keyword: Some("c".to_owned()),
129                }),
130            ],
131        });
132        let json = serde_json::to_string(&filter).expect("serialize");
133        let back: Filter<Cond> = serde_json::from_str(&json).expect("deserialize");
134        assert_eq!(filter, back);
135    }
136}