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}