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}