1use std::fmt;
40
41#[derive(Debug, Clone, PartialEq)]
43pub enum ExtensionValue {
44 Null,
46 Bool(bool),
48 Number(String),
50 String(String),
52 Object(Extensions),
54 Array(Vec<Self>),
56 Raw(String),
62}
63
64impl From<serde_json::Value> for ExtensionValue {
65 fn from(value: serde_json::Value) -> Self {
72 match value {
73 serde_json::Value::Null => Self::Null,
74 serde_json::Value::Bool(boolean) => Self::Bool(boolean),
75 serde_json::Value::Number(number) => Self::Number(number.to_string()),
76 serde_json::Value::String(string) => Self::String(string),
77 serde_json::Value::Object(object) => Self::Object(Extensions::from_json_object(object)),
78 serde_json::Value::Array(items) => {
79 Self::Array(items.into_iter().map(Self::from).collect())
80 }
81 }
82 }
83}
84
85impl From<&ExtensionValue> for serde_json::Value {
86 fn from(value: &ExtensionValue) -> Self {
94 match value {
95 ExtensionValue::Null => Self::Null,
96 ExtensionValue::Bool(boolean) => Self::Bool(*boolean),
97 ExtensionValue::Number(text) => {
98 text.parse()
103 .map_or_else(|_| Self::String(text.clone()), Self::Number)
104 }
105 ExtensionValue::String(string) => Self::String(string.clone()),
106 ExtensionValue::Object(object) => Self::Object(object.to_json_object()),
107 ExtensionValue::Array(items) => Self::Array(items.iter().map(Self::from).collect()),
108 ExtensionValue::Raw(text) => {
109 text.parse().unwrap_or_else(|_| Self::String(text.clone()))
110 }
111 }
112 }
113}
114
115impl Extensions {
116 #[must_use]
118 pub fn from_json_object(object: serde_json::Map<String, serde_json::Value>) -> Self {
119 Self {
120 entries: object
121 .into_iter()
122 .map(|(key, value)| (key, ExtensionValue::from(value)))
123 .collect(),
124 }
125 }
126
127 pub fn absorb_json(&mut self, object: &serde_json::Map<String, serde_json::Value>) {
129 for (key, value) in object {
130 self.insert(key.clone(), ExtensionValue::from(value.clone()));
131 }
132 }
133
134 pub fn take(&mut self, key: &str) -> Option<ExtensionValue> {
136 let index = self
137 .entries
138 .iter()
139 .position(|(existing, _)| existing == key)?;
140 Some(self.entries.remove(index).1)
141 }
142
143 #[must_use]
145 pub fn to_json_object(&self) -> serde_json::Map<String, serde_json::Value> {
146 self.entries
147 .iter()
148 .map(|(key, value)| (key.clone(), serde_json::Value::from(value)))
149 .collect()
150 }
151}
152
153impl ExtensionValue {
154 #[must_use]
156 pub const fn shape(&self) -> &'static str {
157 match self {
158 Self::Null => "null",
159 Self::Bool(_) => "bool",
160 Self::Number(_) => "number",
161 Self::String(_) => "string",
162 Self::Object(_) => "object",
163 Self::Array(_) => "array",
164 Self::Raw(_) => "raw",
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Default)]
171pub struct Extensions {
172 entries: Vec<(String, ExtensionValue)>,
173}
174
175impl Extensions {
176 #[must_use]
178 pub const fn empty() -> Self {
179 Self {
180 entries: Vec::new(),
181 }
182 }
183
184 #[must_use]
186 pub fn with(mut self, key: impl Into<String>, value: ExtensionValue) -> Self {
187 self.entries.push((key.into(), value));
188 self
189 }
190
191 pub fn insert(&mut self, key: impl Into<String>, value: ExtensionValue) {
193 self.entries.push((key.into(), value));
194 }
195
196 #[must_use]
201 pub fn get(&self, key: &str) -> Option<&ExtensionValue> {
202 self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
203 }
204
205 pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a ExtensionValue> {
207 self.entries
208 .iter()
209 .filter(move |(k, _)| k == key)
210 .map(|(_, v)| v)
211 }
212
213 #[must_use]
215 pub fn contains(&self, key: &str) -> bool {
216 self.entries.iter().any(|(k, _)| k == key)
217 }
218
219 pub fn iter(&self) -> impl Iterator<Item = (&str, &ExtensionValue)> {
221 self.entries.iter().map(|(k, v)| (k.as_str(), v))
222 }
223
224 #[must_use]
226 pub const fn is_empty(&self) -> bool {
227 self.entries.is_empty()
228 }
229
230 #[must_use]
232 pub const fn len(&self) -> usize {
233 self.entries.len()
234 }
235
236 #[must_use]
238 pub fn into_entries(self) -> Vec<(String, ExtensionValue)> {
239 self.entries
240 }
241}
242
243impl fmt::Display for Extensions {
244 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245 let mut first = true;
246 for (key, value) in &self.entries {
247 if !first {
248 formatter.write_str(", ")?;
249 }
250 first = false;
251 write!(formatter, "{key}={}", value.shape())?;
252 }
253 Ok(())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 #![expect(
260 clippy::panic,
261 clippy::unwrap_used,
262 reason = "tests assert on fallible calls and unwrap with the failing precondition in the message"
263 )]
264
265 use super::{ExtensionValue, Extensions};
266
267 #[test]
268 fn an_empty_bag_has_nothing() {
269 let bag = Extensions::empty();
270 assert!(bag.is_empty());
271 assert_eq!(bag.len(), 0);
272 assert!(!bag.contains("x"));
273 assert!(bag.get("x").is_none());
274 assert_eq!(bag.iter().count(), 0);
275 }
276
277 #[test]
279 fn insertion_order_is_preserved() {
280 let bag = Extensions::empty()
281 .with("z", ExtensionValue::Number("1".into()))
282 .with("a", ExtensionValue::Null)
283 .with("m", ExtensionValue::Bool(true));
284 let keys: Vec<&str> = bag.iter().map(|(k, _)| k).collect();
285 assert_eq!(keys, vec!["z", "a", "m"]);
286 }
287
288 #[test]
291 fn duplicate_keys_are_kept() {
292 let bag = Extensions::empty()
293 .with("k", ExtensionValue::String("first".into()))
294 .with("k", ExtensionValue::String("second".into()));
295 assert_eq!(bag.len(), 2);
296 assert_eq!(bag.get_all("k").count(), 2);
297 assert_eq!(bag.get("k"), Some(&ExtensionValue::String("first".into())));
298 assert_eq!(
299 bag.get_all("k").collect::<Vec<_>>(),
300 vec![
301 &ExtensionValue::String("first".into()),
302 &ExtensionValue::String("second".into()),
303 ],
304 );
305 }
306
307 #[test]
308 fn nested_shapes_round_trip_by_construction() {
309 let inner = Extensions::empty().with("deep", ExtensionValue::Bool(false));
310 let value = ExtensionValue::Object(inner);
311 let bag = Extensions::empty().with("outer", value);
312 assert_eq!(bag.get("outer").unwrap().shape(), "object");
313 assert_eq!(
314 bag.get("outer"),
315 Some(&ExtensionValue::Object(
316 Extensions::empty().with("deep", ExtensionValue::Bool(false)),
317 )),
318 );
319 }
320
321 #[test]
322 fn arrays_keep_their_order() {
323 let value = ExtensionValue::Array(vec![
324 ExtensionValue::Number("1".into()),
325 ExtensionValue::String("two".into()),
326 ExtensionValue::Null,
327 ]);
328 let bag = Extensions::empty().with("a", value.clone());
329 assert_eq!(bag.get("a"), Some(&value));
330 }
331
332 #[test]
335 fn raw_carries_text_that_did_not_parse() {
336 let bag = Extensions::empty().with("junk", ExtensionValue::Raw("{oops".into()));
337 assert_eq!(bag.get("junk").unwrap().shape(), "raw");
338 assert_eq!(bag.get("junk"), Some(&ExtensionValue::Raw("{oops".into())),);
339 }
340
341 #[test]
342 fn numbers_are_kept_as_text() {
343 for text in ["1.0", "1e10", "10000000000000000000", "0.1", "-0"] {
345 let bag = Extensions::empty().with("n", ExtensionValue::Number(text.into()));
346 assert_eq!(
347 bag.get("n"),
348 Some(&ExtensionValue::Number(text.into())),
349 "{text}"
350 );
351 }
352 }
353
354 #[test]
355 fn into_entries_preserves_order_and_duplicates() {
356 let bag = Extensions::empty()
357 .with("a", ExtensionValue::Null)
358 .with("b", ExtensionValue::Null)
359 .with("a", ExtensionValue::Bool(true));
360 let entries = bag.into_entries();
361 assert_eq!(entries.len(), 3);
362 let (Some(first), Some(last)) = (entries.first(), entries.last()) else {
363 panic!("expected three entries");
364 };
365 assert_eq!(first.0, "a");
366 assert_eq!(last.0, "a");
367 }
368
369 #[test]
370 fn display_summarises_shapes_for_logs() {
371 let bag = Extensions::empty()
372 .with("a", ExtensionValue::String("x".into()))
373 .with("b", ExtensionValue::Object(Extensions::empty()));
374 assert_eq!(bag.to_string(), "a=string, b=object");
375 assert_eq!(Extensions::empty().to_string(), "");
376 }
377}
378
379#[cfg(test)]
380mod json_tests {
381 #![expect(
382 clippy::expect_used,
383 clippy::unwrap_used,
384 reason = "tests assert on fallible conversions; the source is a literal object, so a failure means the literal changed"
385 )]
386
387 use super::{ExtensionValue, Extensions};
388 use serde_json::Value;
389
390 #[test]
395 fn a_bag_survives_the_json_round_trip() {
396 for source in [
397 serde_json::json!({}),
398 serde_json::json!({"a": 1}),
399 serde_json::json!({"a": null, "b": true, "c": "text"}),
400 serde_json::json!({"big": 9_007_199_254_740_993_u64}),
401 serde_json::json!({"float": 1.0}),
402 serde_json::json!({"nested": {"deep": {"deeper": [1, "two", null]}}}),
403 serde_json::json!({"array": [1, 2, {"three": 3}]}),
404 ] {
405 let bag = Extensions::from_json_object(source.as_object().unwrap().clone());
406 let back = bag.to_json_object();
407 assert_eq!(Value::Object(back), source);
408 }
409 }
410
411 #[test]
414 fn a_large_integer_is_not_rounded() {
415 let bag = Extensions::from_json_object(
416 serde_json::json!({ "n": 9_007_199_254_740_993_u64 })
417 .as_object()
418 .expect("object")
419 .clone(),
420 );
421 let back = bag.to_json_object();
422 assert_eq!(
423 back.get("n").and_then(Value::as_u64),
424 Some(9_007_199_254_740_993),
425 );
426 }
427
428 #[test]
434 fn keys_keep_their_set_regardless_of_order() {
435 let source = serde_json::json!({"zebra": 1, "apple": 2, "mango": 3});
436 let bag = Extensions::from_json_object(source.as_object().expect("object").clone());
437 let mut keys: Vec<&str> = bag.iter().map(|(key, _)| key).collect();
438 let mut want = vec!["zebra", "apple", "mango"];
439 keys.sort_unstable();
440 want.sort_unstable();
441 assert_eq!(keys, want);
442 let back = bag.to_json_object();
444 assert_eq!(back.get("zebra").and_then(Value::as_i64), Some(1));
445 assert_eq!(back.get("apple").and_then(Value::as_i64), Some(2));
446 assert_eq!(back.get("mango").and_then(Value::as_i64), Some(3));
447 }
448
449 #[test]
453 fn absorb_and_take_move_members_between_representations() {
454 let mut bag = Extensions::empty();
455 bag.absorb_json(
456 &serde_json::json!({"kept": 1, "took": "2"})
457 .as_object()
458 .expect("object")
459 .clone(),
460 );
461 assert_eq!(bag.len(), 2);
462 assert!(bag.contains("kept"));
463 assert!(bag.contains("took"));
464
465 let taken = bag.take("took");
466 assert_eq!(taken, Some(ExtensionValue::String("2".to_owned())));
467 assert_eq!(bag.len(), 1);
468 assert!(!bag.contains("took"));
469 assert!(bag.contains("kept"));
470
471 assert_eq!(bag.take("absent"), None);
473 assert_eq!(bag.to_json_object().len(), 1);
474 }
475
476 #[test]
479 fn raw_text_is_forwarded_rather_than_dropped() {
480 let bag = Extensions::empty().with("odd", ExtensionValue::Raw("not json".to_owned()));
481 let back = bag.to_json_object();
482 assert_eq!(back.get("odd"), Some(&Value::String("not json".to_owned())));
483 }
484}