Skip to main content

contextgraph_types/
extension.rs

1//! [`ExtensionValue`] — this crate's own JSON value model, for the parts of the
2//! wire the reference types deliberately leave open.
3//!
4//! # Why a value model here at all
5//!
6//! `SPEC.md` §13 U1 requires a receiver to **ignore** members it does not
7//! recognise, and §13 U3 makes namespaced extension members (`vendor:name`) a
8//! first-class part of the wire. "Ignore" is not "discard": a host that relays a
9//! record, or content-addresses one with
10//! [`record_hash_of`](crate::record_attest::record_hash_of), has to carry the
11//! members it does not understand through untouched, or it changes the bytes it
12//! was handed. So the reference types need somewhere to *keep* an unmodelled
13//! member, and keeping one requires a type that can hold any JSON value.
14//!
15//! # Why not `serde_json::Value`
16//!
17//! `contextgraph-types` is **MIT licensed with zero dependencies beyond
18//! `serde`** — that promise is in the crate's `Cargo.toml`, its `README.md`, and
19//! the lib docs, and it is the reason a third party can implement CGP without
20//! adopting the rest of this repository's dependency tree. `serde_json` is an
21//! *optional* dependency behind the `record-hash` feature; [`record`](crate::record)
22//! compiles with no features at all. Reaching for `serde_json::Value` here would
23//! mean one of two bad trades:
24//!
25//! - make `serde_json` non-optional, breaking the zero-dependency promise for
26//!   every consumer that only wants the wire types; or
27//! - feature-gate the field, so [`ContextRecord`](crate::ContextRecord)'s
28//!   *shape* — and therefore what it round-trips — would depend on a Cargo
29//!   feature. A type that silently drops wire members unless a feature is on is
30//!   a worse version of the defect this exists to fix.
31//!
32//! `ExtensionValue` is the third option: about a hundred lines of `serde`
33//! plumbing, no dependency, and the same shape in every build.
34//!
35//! # Fidelity
36//!
37//! The variants mirror the JSON data model exactly, including the distinction
38//! `serde_json::Value` draws between an unsigned integer, a signed integer, and
39//! a float — because that distinction is what decides whether `7` round-trips as
40//! `7` or as `7.0`, and a digit that changes changes the digest. An
41//! [`Object`](ExtensionValue::Object) is a [`BTreeMap`], so members are held in
42//! sorted order; that is invisible to the record hash, which canonicalizes with
43//! RFC 8785 (JCS) and sorts members anyway.
44
45use std::collections::BTreeMap;
46use std::fmt;
47
48use serde::de::{IgnoredAny, MapAccess, SeqAccess, Visitor};
49use serde::ser::{SerializeMap, SerializeSeq};
50use serde::{Deserialize, Deserializer, Serialize, Serializer};
51
52/// Any JSON value, modelled without a JSON dependency.
53///
54/// Used for the members of the wire the reference types leave open:
55/// [`ContextRecord::extensions`](crate::ContextRecord::extensions) and
56/// [`ContextRecord::extra`](crate::ContextRecord::extra).
57#[derive(Debug, Clone, PartialEq)]
58pub enum ExtensionValue {
59    /// JSON `null`.
60    Null,
61    /// JSON `true` / `false`.
62    Bool(bool),
63    /// A non-negative integer that fits in a `u64`.
64    UnsignedInteger(u64),
65    /// A negative integer that fits in an `i64`.
66    SignedInteger(i64),
67    /// A number that is not an integer, or one outside the integer range.
68    Float(f64),
69    /// A JSON string.
70    String(String),
71    /// A JSON array.
72    Array(Vec<ExtensionValue>),
73    /// A JSON object. Sorted by member name; the record hash sorts anyway.
74    Object(BTreeMap<String, ExtensionValue>),
75}
76
77impl ExtensionValue {
78    /// Whether this is JSON `null`.
79    pub fn is_null(&self) -> bool {
80        matches!(self, Self::Null)
81    }
82
83    /// The string, if this is a JSON string.
84    pub fn as_str(&self) -> Option<&str> {
85        match self {
86            Self::String(text) => Some(text),
87            _ => None,
88        }
89    }
90
91    /// The boolean, if this is a JSON boolean.
92    pub fn as_bool(&self) -> Option<bool> {
93        match self {
94            Self::Bool(flag) => Some(*flag),
95            _ => None,
96        }
97    }
98
99    /// The value as an `f64`, if this is any JSON number. Lossy for integers
100    /// beyond 2^53, which is why the integer variants exist separately.
101    pub fn as_f64(&self) -> Option<f64> {
102        match self {
103            Self::UnsignedInteger(value) => Some(*value as f64),
104            Self::SignedInteger(value) => Some(*value as f64),
105            Self::Float(value) => Some(*value),
106            _ => None,
107        }
108    }
109
110    /// The elements, if this is a JSON array.
111    pub fn as_array(&self) -> Option<&[ExtensionValue]> {
112        match self {
113            Self::Array(items) => Some(items),
114            _ => None,
115        }
116    }
117
118    /// The members, if this is a JSON object.
119    pub fn as_object(&self) -> Option<&BTreeMap<String, ExtensionValue>> {
120        match self {
121            Self::Object(members) => Some(members),
122            _ => None,
123        }
124    }
125}
126
127impl From<&str> for ExtensionValue {
128    fn from(text: &str) -> Self {
129        Self::String(text.to_string())
130    }
131}
132
133impl From<String> for ExtensionValue {
134    fn from(text: String) -> Self {
135        Self::String(text)
136    }
137}
138
139impl Serialize for ExtensionValue {
140    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
141        match self {
142            // `serialize_unit` rather than `serialize_none`: a JSON `null` that
143            // is a *value* is not an absent member, and only the former
144            // survives a `Serializer` that skips `None`.
145            Self::Null => serializer.serialize_unit(),
146            Self::Bool(flag) => serializer.serialize_bool(*flag),
147            Self::UnsignedInteger(value) => serializer.serialize_u64(*value),
148            Self::SignedInteger(value) => serializer.serialize_i64(*value),
149            Self::Float(value) => serializer.serialize_f64(*value),
150            Self::String(text) => serializer.serialize_str(text),
151            Self::Array(items) => {
152                let mut seq = serializer.serialize_seq(Some(items.len()))?;
153                for item in items {
154                    seq.serialize_element(item)?;
155                }
156                seq.end()
157            }
158            Self::Object(members) => {
159                let mut map = serializer.serialize_map(Some(members.len()))?;
160                for (name, value) in members {
161                    map.serialize_entry(name, value)?;
162                }
163                map.end()
164            }
165        }
166    }
167}
168
169impl<'de> Deserialize<'de> for ExtensionValue {
170    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
171        deserializer.deserialize_any(ExtensionValueVisitor)
172    }
173}
174
175struct ExtensionValueVisitor;
176
177impl<'de> Visitor<'de> for ExtensionValueVisitor {
178    type Value = ExtensionValue;
179
180    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181        formatter.write_str("any JSON value")
182    }
183
184    fn visit_unit<E>(self) -> Result<Self::Value, E> {
185        Ok(ExtensionValue::Null)
186    }
187
188    fn visit_none<E>(self) -> Result<Self::Value, E> {
189        Ok(ExtensionValue::Null)
190    }
191
192    fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
193        deserializer.deserialize_any(self)
194    }
195
196    fn visit_bool<E>(self, flag: bool) -> Result<Self::Value, E> {
197        Ok(ExtensionValue::Bool(flag))
198    }
199
200    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
201        Ok(ExtensionValue::UnsignedInteger(value))
202    }
203
204    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
205        // A non-negative integer is held in the unsigned variant whichever
206        // visitor method delivered it, so `1` compares equal to `1` no matter
207        // which side of the wire it came from.
208        Ok(match u64::try_from(value) {
209            Ok(unsigned) => ExtensionValue::UnsignedInteger(unsigned),
210            Err(_) => ExtensionValue::SignedInteger(value),
211        })
212    }
213
214    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
215        Ok(ExtensionValue::Float(value))
216    }
217
218    fn visit_str<E>(self, text: &str) -> Result<Self::Value, E> {
219        Ok(ExtensionValue::String(text.to_string()))
220    }
221
222    fn visit_string<E>(self, text: String) -> Result<Self::Value, E> {
223        Ok(ExtensionValue::String(text))
224    }
225
226    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
227        let mut items = Vec::with_capacity(seq.size_hint().unwrap_or(0));
228        while let Some(item) = seq.next_element()? {
229            items.push(item);
230        }
231        Ok(ExtensionValue::Array(items))
232    }
233
234    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
235        let mut members = BTreeMap::new();
236        while let Some(name) = map.next_key::<String>()? {
237            members.insert(name, map.next_value()?);
238        }
239        Ok(ExtensionValue::Object(members))
240    }
241}
242
243/// Read one map entry into an [`ExtensionValue`], or discard it.
244///
245/// Shared by the record's extension plumbing: a member the reference types
246/// already model must still have its value consumed from the [`MapAccess`], and
247/// consuming it as [`IgnoredAny`] is both cheaper and unconditionally
248/// infallible compared with parsing a value that is about to be thrown away.
249pub(crate) fn take_or_ignore<'de, A: MapAccess<'de>>(
250    map: &mut A,
251    keep: bool,
252) -> Result<Option<ExtensionValue>, A::Error> {
253    if keep {
254        map.next_value().map(Some)
255    } else {
256        map.next_value::<IgnoredAny>().map(|_| None)
257    }
258}