Skip to main content

aitp_core/
extensions.rs

1//! Forward-compatible extension fields.
2//!
3//! Per RFC-AITP-0001 §6, every signed object reserves an `extensions` slot.
4//! Unknown JSON fields _outside_ `extensions` MUST be rejected (because
5//! signature canonicalization depends on the exact field set). Unknown keys
6//! _inside_ `extensions` MAY be ignored.
7
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11/// A map of vendor-namespaced extension values.
12///
13/// Keys SHOULD use a reverse-DNS-style prefix (e.g. `vendor.example/feature`)
14/// to avoid collisions across implementations. The map preserves insertion
15/// order for stable canonicalization.
16#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(transparent)]
18pub struct ExtensionsMap(BTreeMap<String, serde_json::Value>);
19
20impl ExtensionsMap {
21    /// Construct an empty extensions map.
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// True if no extensions are set.
27    ///
28    /// Used with `#[serde(skip_serializing_if = "ExtensionsMap::is_empty")]`
29    /// so that empty extensions are omitted from canonical JSON entirely
30    /// rather than serialized as `"extensions":{}` — this matters for
31    /// signature interop.
32    pub fn is_empty(&self) -> bool {
33        self.0.is_empty()
34    }
35
36    /// Insert a key/value pair.
37    pub fn insert(&mut self, key: impl Into<String>, value: serde_json::Value) {
38        self.0.insert(key.into(), value);
39    }
40
41    /// Get a value by key.
42    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
43        self.0.get(key)
44    }
45
46    /// Iterate over key/value pairs in canonical (sorted) order.
47    pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
48        self.0.iter()
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use serde::{Deserialize, Serialize};
56    use serde_json::json;
57
58    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
59    struct Holder {
60        #[serde(default, skip_serializing_if = "ExtensionsMap::is_empty")]
61        extensions: ExtensionsMap,
62    }
63
64    #[test]
65    fn empty_is_empty() {
66        let m = ExtensionsMap::new();
67        assert!(m.is_empty());
68    }
69
70    #[test]
71    fn after_insert_not_empty() {
72        let mut m = ExtensionsMap::new();
73        m.insert("vendor.example/foo", json!({"x": 1}));
74        assert!(!m.is_empty());
75        assert_eq!(m.get("vendor.example/foo"), Some(&json!({"x": 1})));
76    }
77
78    #[test]
79    fn round_trip_with_extension() {
80        let mut m = ExtensionsMap::new();
81        m.insert("vendor.example/foo", json!({"x": 1}));
82        let h = Holder { extensions: m };
83        let s = serde_json::to_string(&h).unwrap();
84        assert!(s.contains("vendor.example/foo"));
85        let back: Holder = serde_json::from_str(&s).unwrap();
86        assert_eq!(h, back);
87    }
88
89    #[test]
90    fn empty_extensions_omitted_from_canonical_json() {
91        let h = Holder {
92            extensions: ExtensionsMap::new(),
93        };
94        let s = serde_json::to_string(&h).unwrap();
95        // skip_serializing_if drops the field entirely when empty.
96        assert_eq!(s, "{}");
97    }
98
99    #[test]
100    fn keys_iterate_in_sorted_order() {
101        let mut m = ExtensionsMap::new();
102        m.insert("z", json!(1));
103        m.insert("a", json!(2));
104        m.insert("m", json!(3));
105        let order: Vec<_> = m.iter().map(|(k, _)| k.as_str()).collect();
106        assert_eq!(order, vec!["a", "m", "z"]);
107    }
108}