Skip to main content

jmap_base_client/
push.rs

1//! Canonical push notification types shared by SSE and WebSocket transports.
2//! Spec: RFC 8620 §7.1 (Push Subscriptions)
3
4use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use jmap_types::{Id, State};
9
10/// A state change push notification (RFC 8620 §7.1).
11///
12/// Sent over both SSE (as a push event) and WebSocket (as a frame type).
13#[non_exhaustive]
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct StateChange {
16    /// For each account that changed: maps data-type name to the new [`State`] token.
17    ///
18    /// Outer key: account [`Id`].  Inner key: JMAP data-type name (e.g. `"Email"`).
19    /// Inner value: new opaque state string; pass to `Email/changes` etc. as `sinceState`.
20    pub changed: HashMap<Id, HashMap<String, State>>,
21
22    /// Catch-all for vendor / site / private extension fields not covered
23    /// by the typed fields above. Preserves unknown fields across
24    /// deserialize/serialize round-trip per workspace extras-preservation
25    /// policy (see workspace AGENTS.md).
26    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
27    pub extra: serde_json::Map<String, serde_json::Value>,
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use serde_json::json;
34
35    /// `StateChange.extra` captures unknown fields on deserialize and
36    /// flattens them on serialize (round-trip).
37    #[test]
38    fn state_change_preserves_vendor_extras() {
39        let raw = json!({
40            "changed": {
41                "acc1": { "Email": "s42" }
42            },
43            "acmeCorpSequence": 17
44        });
45        let obj: StateChange = serde_json::from_value(raw).expect("StateChange must deserialize");
46        assert_eq!(
47            obj.extra.get("acmeCorpSequence").and_then(|v| v.as_u64()),
48            Some(17)
49        );
50
51        // Round-trip: serializing back must reproduce the vendor field.
52        let v = serde_json::to_value(&obj).expect("StateChange must serialize");
53        assert_eq!(v["acmeCorpSequence"], json!(17));
54    }
55}