Skip to main content

jmap_cid_types/
capability.rs

1//! draft-atwood-jmap-cid-00 §3 — capability registration and
2//! capability value object.
3//!
4//! Provides the capability URI constant [`JMAP_CID_URI`] and the
5//! [`CidCapability`] value object that mirrors the wire shape of
6//! the capability's value.
7
8use serde::{Deserialize, Serialize};
9
10/// The JMAP capability URI for the Blob Content Identifiers
11/// extension (draft-atwood-jmap-cid-00 §3).
12///
13/// Present as a key in both the session-level `capabilities` object
14/// and in each account's `accountCapabilities` object. The value of
15/// the key is a (currently empty) JSON object — see
16/// [`CidCapability`] for the typed wire shape.
17pub const JMAP_CID_URI: &str = "urn:ietf:params:jmap:cid";
18
19/// Value object of the `urn:ietf:params:jmap:cid` capability
20/// (draft-atwood-jmap-cid-00 §3).
21///
22/// The draft currently specifies an empty value object: when a
23/// server advertises CID, the value of the
24/// [`JMAP_CID_URI`] key in both the session-level `capabilities`
25/// object and per-account `accountCapabilities` is `{}`. The
26/// `#[non_exhaustive]` attribute keeps the door open for the draft
27/// to add capability-level fields (e.g. an enumerated digest
28/// algorithm set when CID generalises beyond SHA-256) without a
29/// breaking change to consumers that destructure the struct.
30///
31/// The [`extra`](Self::extra) field is the workspace
32/// extras-preservation surface (workspace AGENTS.md
33/// "extras-preservation policy"): unknown vendor / site / future
34/// IETF fields on the wire JSON survive deserialize and serialize
35/// untouched.
36///
37/// ## Example
38///
39/// ```
40/// use jmap_cid_types::CidCapability;
41///
42/// // Empty value object per the current draft revision.
43/// let cap: CidCapability = serde_json::from_str("{}")?;
44/// assert!(cap.extra.is_empty());
45///
46/// // Vendor extension survives round-trip.
47/// let cap: CidCapability = serde_json::from_str(
48///     r#"{"acmeCorpFastDigest": true}"#,
49/// )?;
50/// assert_eq!(
51///     cap.extra.get("acmeCorpFastDigest"),
52///     Some(&serde_json::Value::Bool(true)),
53/// );
54/// # Ok::<(), Box<dyn std::error::Error>>(())
55/// ```
56#[non_exhaustive]
57#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct CidCapability {
60    /// Catch-all for vendor / site / future-IETF fields not
61    /// covered by the typed fields above. Preserves unknown fields
62    /// across deserialize/serialize round-trip per workspace policy.
63    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
64    pub extra: serde_json::Map<String, serde_json::Value>,
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn empty_object_round_trips() {
73        // Oracle: draft-atwood-jmap-cid-00 §3 — value object is
74        // currently empty `{}`.
75        let cap: CidCapability = serde_json::from_str("{}").unwrap();
76        assert!(cap.extra.is_empty());
77        let json = serde_json::to_string(&cap).unwrap();
78        assert_eq!(json, "{}");
79    }
80
81    #[test]
82    fn vendor_extras_preserved_through_round_trip() {
83        // Workspace extras-preservation policy (JMAP-lbdy):
84        // unknown fields survive deserialize and re-emerge on
85        // serialize.
86        let input = r#"{"acmeCorpFastDigest":true,"vendor.example:limit":1024}"#;
87        let cap: CidCapability = serde_json::from_str(input).unwrap();
88        assert_eq!(cap.extra.len(), 2);
89        assert_eq!(
90            cap.extra.get("acmeCorpFastDigest"),
91            Some(&serde_json::Value::Bool(true)),
92        );
93        assert_eq!(
94            cap.extra.get("vendor.example:limit"),
95            Some(&serde_json::Value::Number(1024.into())),
96        );
97        // Serialize and deserialize again to assert byte-shape
98        // round-trip (key order is preserve by serde_json::Map).
99        let round_tripped = serde_json::to_string(&cap).unwrap();
100        let cap2: CidCapability = serde_json::from_str(&round_tripped).unwrap();
101        assert_eq!(cap, cap2);
102    }
103
104    #[test]
105    fn default_constructs_empty() {
106        let cap = CidCapability::default();
107        assert!(cap.extra.is_empty());
108        let json = serde_json::to_string(&cap).unwrap();
109        assert_eq!(json, "{}");
110    }
111}