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}