Skip to main content

vantage_table/
cbor_ext.rs

1//! Convenience accessors for `ciborium::Value` that mirror the muscle-memory
2//! API of `serde_json::Value`.
3//!
4//! ciborium's native `Value` already provides `as_text`, `as_integer`,
5//! `as_float`, `as_array`, `as_map`, `as_bytes` and friends. What it lacks —
6//! and what code reading typed records reaches for constantly — are the
7//! shorter names (`as_str`, `as_i64`) and string-keyed `Map` lookup. This
8//! trait fills exactly that gap; nothing more.
9
10use ciborium::Value as CborValue;
11
12pub trait CborValueExt {
13    /// Borrow the inner string if this is a `Text` value.
14    fn as_str(&self) -> Option<&str>;
15
16    /// Try to extract an `i64`. Returns `None` if the value is not an
17    /// integer or doesn't fit.
18    fn as_i64(&self) -> Option<i64>;
19
20    /// Try to extract a `u64`. Returns `None` if the value is not an
21    /// integer or doesn't fit.
22    fn as_u64(&self) -> Option<u64>;
23
24    /// Borrow the inner `f64` if this is a `Float` value.
25    fn as_f64(&self) -> Option<f64>;
26
27    /// Look up a value in a `Map` by string key. Returns `None` if the
28    /// value isn't a map or the key isn't present.
29    fn get(&self, key: &str) -> Option<&CborValue>;
30
31    /// Same as [`get`](Self::get), but returns a mutable reference.
32    fn get_mut(&mut self, key: &str) -> Option<&mut CborValue>;
33}
34
35impl CborValueExt for CborValue {
36    fn as_str(&self) -> Option<&str> {
37        self.as_text()
38    }
39
40    fn as_i64(&self) -> Option<i64> {
41        self.as_integer().and_then(|i| i64::try_from(i).ok())
42    }
43
44    fn as_u64(&self) -> Option<u64> {
45        self.as_integer().and_then(|i| u64::try_from(i).ok())
46    }
47
48    fn as_f64(&self) -> Option<f64> {
49        self.as_float()
50    }
51
52    fn get(&self, key: &str) -> Option<&CborValue> {
53        self.as_map()?
54            .iter()
55            .find(|(k, _)| k.as_text() == Some(key))
56            .map(|(_, v)| v)
57    }
58
59    fn get_mut(&mut self, key: &str) -> Option<&mut CborValue> {
60        self.as_map_mut()?
61            .iter_mut()
62            .find(|(k, _)| k.as_text() == Some(key))
63            .map(|(_, v)| v)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    fn map(pairs: Vec<(&str, CborValue)>) -> CborValue {
72        CborValue::Map(
73            pairs
74                .into_iter()
75                .map(|(k, v)| (CborValue::Text(k.to_string()), v))
76                .collect(),
77        )
78    }
79
80    #[test]
81    fn as_str_and_int_accessors() {
82        let v = CborValue::Text("hello".into());
83        assert_eq!(v.as_str(), Some("hello"));
84        assert_eq!(v.as_i64(), None);
85
86        let n = CborValue::Integer(42i64.into());
87        assert_eq!(n.as_i64(), Some(42));
88        assert_eq!(n.as_u64(), Some(42));
89        assert_eq!(n.as_str(), None);
90    }
91
92    #[test]
93    fn map_lookup_by_str_key() {
94        let mut record = map(vec![
95            ("name", CborValue::Text("alice".into())),
96            ("age", CborValue::Integer(30i64.into())),
97        ]);
98        assert_eq!(record.get("name").and_then(|v| v.as_str()), Some("alice"));
99        assert_eq!(record.get("age").and_then(|v| v.as_i64()), Some(30));
100        assert!(record.get("missing").is_none());
101
102        // mutable lookup
103        if let Some(age) = record.get_mut("age") {
104            *age = CborValue::Integer(31i64.into());
105        }
106        assert_eq!(record.get("age").and_then(|v| v.as_i64()), Some(31));
107    }
108}