Skip to main content

slim_auth/
metadata.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Generic, arbitrarily nested metadata map structure that can be attached to
5//! configuration structs. Each value can be a string, number, list or another map.
6//! This provides an escape hatch for custom configuration without changing the
7//! strongly typed configuration surface.
8
9use std::collections::HashMap;
10use std::convert::TryFrom;
11use std::error::Error;
12use std::fmt;
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17/// A generic metadata value.
18///
19/// Supported variants:
20/// - String
21/// - Bool
22/// - Number (serde_json::Number – can represent integer & floating point)
23/// - List (Vec<MetadataValue>)
24/// - Map (nested MetadataMap)
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
26#[serde(untagged)]
27pub enum MetadataValue {
28    String(String),
29    Bool(bool),
30    Number(serde_json::Number),
31    List(Vec<MetadataValue>),
32    Map(MetadataMap),
33}
34
35impl MetadataValue {
36    /// Return the variant name (for error reporting).
37    fn variant_name(&self) -> &'static str {
38        match self {
39            MetadataValue::String(_) => "String",
40            MetadataValue::Bool(_) => "Bool",
41            MetadataValue::Number(_) => "Number",
42            MetadataValue::List(_) => "List",
43            MetadataValue::Map(_) => "Map",
44        }
45    }
46
47    /// Convenience accessors.
48    pub fn as_str(&self) -> Option<&str> {
49        match self {
50            MetadataValue::String(s) => Some(s),
51            _ => None,
52        }
53    }
54
55    pub fn as_number(&self) -> Option<&serde_json::Number> {
56        match self {
57            MetadataValue::Number(n) => Some(n),
58            _ => None,
59        }
60    }
61
62    pub fn as_bool(&self) -> Option<bool> {
63        match self {
64            MetadataValue::Bool(v) => Some(*v),
65            _ => None,
66        }
67    }
68
69    pub fn as_list(&self) -> Option<&[MetadataValue]> {
70        match self {
71            MetadataValue::List(v) => Some(v.as_slice()),
72            _ => None,
73        }
74    }
75
76    pub fn as_map(&self) -> Option<&MetadataMap> {
77        match self {
78            MetadataValue::Map(m) => Some(m),
79            _ => None,
80        }
81    }
82}
83
84/// Error type returned when attempting to convert (try_into) a `MetadataValue`
85/// into a concrete type and the value does not have the expected shape.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum MetadataConversionError {
88    WrongType {
89        expected: &'static str,
90        found: &'static str,
91    },
92    NumberOutOfRange {
93        expected: &'static str,
94        found: serde_json::Number,
95    },
96    /// Returned when a floating point number cannot be represented (unlikely here,
97    /// but preserved for completeness).
98    FloatNotRepresentable,
99}
100
101impl fmt::Display for MetadataConversionError {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            MetadataConversionError::WrongType { expected, found } => {
105                write!(
106                    f,
107                    "wrong metadata value type: expected {expected}, found {found}"
108                )
109            }
110            MetadataConversionError::NumberOutOfRange { expected, found } => {
111                write!(
112                    f,
113                    "number out of range when converting metadata value: expected {expected}, found {found}"
114                )
115            }
116            MetadataConversionError::FloatNotRepresentable => {
117                write!(f, "float not representable for metadata value")
118            }
119        }
120    }
121}
122
123impl Error for MetadataConversionError {}
124
125impl From<String> for MetadataValue {
126    fn from(value: String) -> Self {
127        MetadataValue::String(value)
128    }
129}
130
131impl From<&str> for MetadataValue {
132    fn from(value: &str) -> Self {
133        MetadataValue::String(value.to_string())
134    }
135}
136
137impl From<i64> for MetadataValue {
138    fn from(value: i64) -> Self {
139        MetadataValue::Number(serde_json::Number::from(value))
140    }
141}
142
143impl From<bool> for MetadataValue {
144    fn from(value: bool) -> Self {
145        MetadataValue::Bool(value)
146    }
147}
148
149impl From<u64> for MetadataValue {
150    fn from(value: u64) -> Self {
151        MetadataValue::Number(serde_json::Number::from(value))
152    }
153}
154
155impl From<f64> for MetadataValue {
156    fn from(value: f64) -> Self {
157        // serde_json::Number cannot represent NaN/Infinity; fall back to string.
158        serde_json::Number::from_f64(value)
159            .map(MetadataValue::Number)
160            .unwrap_or_else(|| MetadataValue::String(value.to_string()))
161    }
162}
163
164impl<T: Into<MetadataValue>> From<Vec<T>> for MetadataValue {
165    fn from(v: Vec<T>) -> Self {
166        MetadataValue::List(v.into_iter().map(|e| e.into()).collect())
167    }
168}
169
170impl From<MetadataMap> for MetadataValue {
171    fn from(m: MetadataMap) -> Self {
172        MetadataValue::Map(m)
173    }
174}
175
176/* -------- TryFrom implementations for extraction -------- */
177
178impl TryFrom<MetadataValue> for String {
179    type Error = MetadataConversionError;
180
181    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
182        match value {
183            MetadataValue::String(s) => Ok(s),
184            other => Err(MetadataConversionError::WrongType {
185                expected: "String",
186                found: other.variant_name(),
187            }),
188        }
189    }
190}
191
192impl<'a> TryFrom<&'a MetadataValue> for &'a str {
193    type Error = MetadataConversionError;
194
195    fn try_from(value: &'a MetadataValue) -> Result<Self, Self::Error> {
196        match value {
197            MetadataValue::String(s) => Ok(s.as_str()),
198            other => Err(MetadataConversionError::WrongType {
199                expected: "String",
200                found: other.variant_name(),
201            }),
202        }
203    }
204}
205
206impl TryFrom<&MetadataValue> for String {
207    type Error = MetadataConversionError;
208
209    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
210        match value {
211            MetadataValue::String(s) => Ok(s.clone()),
212            other => Err(MetadataConversionError::WrongType {
213                expected: "String",
214                found: other.variant_name(),
215            }),
216        }
217    }
218}
219
220impl TryFrom<MetadataValue> for bool {
221    type Error = MetadataConversionError;
222
223    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
224        match value {
225            MetadataValue::Bool(v) => Ok(v),
226            other => Err(MetadataConversionError::WrongType {
227                expected: "Bool",
228                found: other.variant_name(),
229            }),
230        }
231    }
232}
233
234impl TryFrom<&MetadataValue> for bool {
235    type Error = MetadataConversionError;
236
237    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
238        match value {
239            MetadataValue::Bool(v) => Ok(*v),
240            other => Err(MetadataConversionError::WrongType {
241                expected: "Bool",
242                found: other.variant_name(),
243            }),
244        }
245    }
246}
247
248impl TryFrom<MetadataValue> for serde_json::Number {
249    type Error = MetadataConversionError;
250
251    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
252        match value {
253            MetadataValue::Number(n) => Ok(n),
254            other => Err(MetadataConversionError::WrongType {
255                expected: "Number",
256                found: other.variant_name(),
257            }),
258        }
259    }
260}
261
262impl TryFrom<&MetadataValue> for serde_json::Number {
263    type Error = MetadataConversionError;
264
265    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
266        match value {
267            MetadataValue::Number(n) => Ok(n.clone()),
268            other => Err(MetadataConversionError::WrongType {
269                expected: "Number",
270                found: other.variant_name(),
271            }),
272        }
273    }
274}
275
276impl TryFrom<MetadataValue> for i64 {
277    type Error = MetadataConversionError;
278
279    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
280        match value {
281            MetadataValue::Number(n) => {
282                n.as_i64().ok_or(MetadataConversionError::NumberOutOfRange {
283                    expected: "i64",
284                    found: n,
285                })
286            }
287            other => Err(MetadataConversionError::WrongType {
288                expected: "Number",
289                found: other.variant_name(),
290            }),
291        }
292    }
293}
294
295impl TryFrom<&MetadataValue> for i64 {
296    type Error = MetadataConversionError;
297
298    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
299        match value {
300            MetadataValue::Number(n) => {
301                n.as_i64().ok_or(MetadataConversionError::NumberOutOfRange {
302                    expected: "i64",
303                    found: n.clone(),
304                })
305            }
306            other => Err(MetadataConversionError::WrongType {
307                expected: "Number",
308                found: other.variant_name(),
309            }),
310        }
311    }
312}
313
314impl TryFrom<MetadataValue> for u64 {
315    type Error = MetadataConversionError;
316
317    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
318        match value {
319            MetadataValue::Number(n) => {
320                n.as_u64().ok_or(MetadataConversionError::NumberOutOfRange {
321                    expected: "u64",
322                    found: n,
323                })
324            }
325            other => Err(MetadataConversionError::WrongType {
326                expected: "Number",
327                found: other.variant_name(),
328            }),
329        }
330    }
331}
332
333impl TryFrom<&MetadataValue> for u64 {
334    type Error = MetadataConversionError;
335
336    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
337        match value {
338            MetadataValue::Number(n) => {
339                n.as_u64()
340                    .ok_or_else(|| MetadataConversionError::NumberOutOfRange {
341                        expected: "u64",
342                        found: n.clone(),
343                    })
344            }
345            other => Err(MetadataConversionError::WrongType {
346                expected: "Number",
347                found: other.variant_name(),
348            }),
349        }
350    }
351}
352
353impl TryFrom<MetadataValue> for f64 {
354    type Error = MetadataConversionError;
355
356    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
357        match value {
358            MetadataValue::Number(n) => n
359                .as_f64()
360                .ok_or(MetadataConversionError::FloatNotRepresentable),
361            other => Err(MetadataConversionError::WrongType {
362                expected: "Number",
363                found: other.variant_name(),
364            }),
365        }
366    }
367}
368
369impl TryFrom<&MetadataValue> for f64 {
370    type Error = MetadataConversionError;
371
372    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
373        match value {
374            MetadataValue::Number(n) => n
375                .as_f64()
376                .ok_or(MetadataConversionError::FloatNotRepresentable),
377            other => Err(MetadataConversionError::WrongType {
378                expected: "Number",
379                found: other.variant_name(),
380            }),
381        }
382    }
383}
384
385impl TryFrom<MetadataValue> for Vec<MetadataValue> {
386    type Error = MetadataConversionError;
387
388    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
389        match value {
390            MetadataValue::List(v) => Ok(v),
391            other => Err(MetadataConversionError::WrongType {
392                expected: "List",
393                found: other.variant_name(),
394            }),
395        }
396    }
397}
398
399impl TryFrom<&MetadataValue> for Vec<MetadataValue> {
400    type Error = MetadataConversionError;
401
402    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
403        match value {
404            MetadataValue::List(v) => Ok(v.clone()),
405            other => Err(MetadataConversionError::WrongType {
406                expected: "List",
407                found: other.variant_name(),
408            }),
409        }
410    }
411}
412
413impl TryFrom<MetadataValue> for MetadataMap {
414    type Error = MetadataConversionError;
415
416    fn try_from(value: MetadataValue) -> Result<Self, Self::Error> {
417        match value {
418            MetadataValue::Map(m) => Ok(m),
419            other => Err(MetadataConversionError::WrongType {
420                expected: "Map",
421                found: other.variant_name(),
422            }),
423        }
424    }
425}
426
427impl TryFrom<&MetadataValue> for MetadataMap {
428    type Error = MetadataConversionError;
429
430    fn try_from(value: &MetadataValue) -> Result<Self, Self::Error> {
431        match value {
432            MetadataValue::Map(m) => Ok(m.clone()),
433            other => Err(MetadataConversionError::WrongType {
434                expected: "Map",
435                found: other.variant_name(),
436            }),
437        }
438    }
439}
440
441/// A generic metadata map. Newtype with a flattened map so that serde encodes
442/// just a JSON/YAML object and not an inner field name.
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
444pub struct MetadataMap {
445    #[serde(flatten)]
446    pub inner: HashMap<String, MetadataValue>,
447}
448
449impl MetadataMap {
450    /// Create an empty metadata map.
451    pub fn new() -> Self {
452        Self {
453            inner: HashMap::new(),
454        }
455    }
456
457    /// Iterate over key-value pairs.
458    pub fn iter(&self) -> impl Iterator<Item = (&String, &MetadataValue)> {
459        self.inner.iter()
460    }
461
462    /// Insert any value implementing Into<MetadataValue>.
463    pub fn insert<K: Into<String>, V: Into<MetadataValue>>(&mut self, key: K, value: V) {
464        self.inner.insert(key.into(), value.into());
465    }
466
467    /// Get a value by key.
468    pub fn get(&self, key: &str) -> Option<&MetadataValue> {
469        self.inner.get(key)
470    }
471
472    /// Mutable get.
473    pub fn get_mut(&mut self, key: &str) -> Option<&mut MetadataValue> {
474        self.inner.get_mut(key)
475    }
476
477    /// Returns true if map is empty.
478    pub fn is_empty(&self) -> bool {
479        self.inner.is_empty()
480    }
481
482    /// Length of the map.
483    pub fn len(&self) -> usize {
484        self.inner.len()
485    }
486
487    /// Extend metadata map with another.
488    pub fn extend(&mut self, other: MetadataMap) {
489        self.inner.extend(other.inner);
490    }
491}
492
493impl<K, V> std::iter::FromIterator<(K, V)> for MetadataMap
494where
495    K: Into<String>,
496    V: Into<MetadataValue>,
497{
498    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
499        let mut map = MetadataMap::new();
500        for (k, v) in iter {
501            map.insert(k, v);
502        }
503        map
504    }
505}
506
507/// Conversion from MetadataValue to prost_types::Value for protobuf serialization
508impl From<&MetadataValue> for prost_types::Value {
509    fn from(value: &MetadataValue) -> Self {
510        let kind = match value {
511            MetadataValue::String(s) => prost_types::value::Kind::StringValue(s.clone()),
512            MetadataValue::Bool(v) => prost_types::value::Kind::BoolValue(*v),
513            MetadataValue::Number(n) => {
514                // Convert serde_json::Number to f64
515                prost_types::value::Kind::NumberValue(
516                    n.as_f64().unwrap_or_else(|| n.as_i64().unwrap_or(0) as f64),
517                )
518            }
519            MetadataValue::List(list) => {
520                let values = list.iter().map(prost_types::Value::from).collect();
521                prost_types::value::Kind::ListValue(prost_types::ListValue { values })
522            }
523            MetadataValue::Map(map) => {
524                let fields = map
525                    .inner
526                    .iter()
527                    .map(|(k, v)| (k.clone(), prost_types::Value::from(v)))
528                    .collect();
529                prost_types::value::Kind::StructValue(prost_types::Struct { fields })
530            }
531        };
532
533        prost_types::Value { kind: Some(kind) }
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use std::f64;
540
541    use super::*;
542    use serde_json::{Value, json};
543
544    #[test]
545    fn insert_and_get_primitives() {
546        let mut m = MetadataMap::new();
547        m.insert("s", "hello");
548        m.insert("b", true);
549        m.insert("i", 42i64);
550        m.insert("u", 7u64);
551        m.insert("f", std::f64::consts::PI);
552
553        assert!(matches!(m.get("s"), Some(MetadataValue::String(v)) if v == "hello"));
554        assert!(matches!(m.get("b"), Some(MetadataValue::Bool(v)) if *v));
555        assert!(matches!(m.get("i"), Some(MetadataValue::Number(n)) if n.as_i64()==Some(42)));
556        assert!(matches!(m.get("u"), Some(MetadataValue::Number(n)) if n.as_u64()==Some(7)));
557        // Float may become string fallback if NaN; here it's valid
558        assert!(matches!(m.get("f"), Some(MetadataValue::Number(_))));
559    }
560
561    #[test]
562    fn list_and_nested_map() {
563        let mut child = MetadataMap::new();
564        child.insert("k", "v");
565
566        let mut root = MetadataMap::new();
567        root.insert("list", vec![1i64, 2i64, 3i64]);
568        root.insert("child", child.clone());
569
570        match root.get("list").unwrap() {
571            MetadataValue::List(v) => assert_eq!(v.len(), 3),
572            _ => panic!("expected list"),
573        }
574        match root.get("child").unwrap() {
575            MetadataValue::Map(m) => {
576                assert!(matches!(m.get("k"), Some(MetadataValue::String(s)) if s == "v"))
577            }
578            _ => panic!("expected map"),
579        }
580    }
581
582    #[test]
583    fn serialize_to_json() {
584        let mut m = MetadataMap::new();
585        m.insert("name", "slim");
586        m.insert("enabled", true);
587        m.insert("version", 1u64);
588        m.insert("values", vec!["a", "b", "c"]);
589        let mut nested = MetadataMap::new();
590        nested.insert("inner", 10i64);
591        m.insert("nested", nested);
592
593        let json_str = serde_json::to_string(&m).unwrap();
594        let v: Value = serde_json::from_str(&json_str).unwrap();
595        assert_eq!(v["name"], json!("slim"));
596        assert_eq!(v["enabled"], json!(true));
597        assert_eq!(v["version"], json!(1));
598        assert_eq!(v["values"], json!(["a", "b", "c"]));
599        assert_eq!(v["nested"]["inner"], json!(10));
600    }
601
602    #[test]
603    fn deserialize_from_json() {
604        let raw = r#"{
605            "alpha":"a",
606            "enabled": true,
607            "num": 5,
608            "list": [1,2,3],
609            "deep": {"k":"v","n":9}
610        }"#;
611        let m: MetadataMap = serde_json::from_str(raw).unwrap();
612        assert!(matches!(m.get("alpha"), Some(MetadataValue::String(s)) if s=="a"));
613        assert!(matches!(m.get("enabled"), Some(MetadataValue::Bool(v)) if *v));
614        assert!(matches!(m.get("num"), Some(MetadataValue::Number(n)) if n.as_i64()==Some(5)));
615        assert!(matches!(m.get("list"), Some(MetadataValue::List(v)) if v.len()==3));
616        match m.get("deep").unwrap() {
617            MetadataValue::Map(dm) => {
618                assert!(matches!(dm.get("k"), Some(MetadataValue::String(s)) if s=="v"));
619                assert!(
620                    matches!(dm.get("n"), Some(MetadataValue::Number(n)) if n.as_i64()==Some(9))
621                );
622            }
623            _ => panic!("expected deep map"),
624        }
625    }
626
627    #[test]
628    fn overwrite_key() {
629        let mut m = MetadataMap::new();
630        m.insert("k", 1i64);
631        m.insert("k", "now_string");
632        assert!(matches!(m.get("k"), Some(MetadataValue::String(s)) if s=="now_string"));
633    }
634
635    #[test]
636    fn schema_generation() {
637        // Ensure schemars can generate a schema (smoke test)
638        let _schema = schemars::schema_for!(MetadataMap);
639    }
640
641    #[test]
642    fn collect_into_metadata_map() {
643        let pairs = vec![("a", "alpha"), ("b", "beta"), ("c", "gamma")];
644        let m: MetadataMap = pairs.into_iter().collect();
645        assert_eq!(m.get("a").unwrap().as_str(), Some("alpha"));
646        assert_eq!(m.get("b").unwrap().as_str(), Some("beta"));
647        assert_eq!(m.get("c").unwrap().as_str(), Some("gamma"));
648    }
649
650    #[test]
651    fn try_from_string_ok() {
652        let v: MetadataValue = "hello".into();
653        let s: String = v.clone().try_into().unwrap();
654        assert_eq!(s, "hello");
655        let s_ref: &str = (&v).try_into().unwrap();
656        assert_eq!(s_ref, "hello");
657    }
658
659    #[test]
660    fn try_from_number_variants() {
661        let vu: MetadataValue = 10u64.into();
662        assert_eq!(u64::try_from(vu.clone()).unwrap(), 10);
663        assert_eq!(i64::try_from(vu.clone()).unwrap(), 10);
664        let vf: MetadataValue = f64::consts::PI.into();
665        let f = f64::try_from(vf.clone()).unwrap();
666        assert!((f - f64::consts::PI).abs() < 1e-10);
667    }
668
669    #[test]
670    fn try_from_wrong_type_errors() {
671        let v: MetadataValue = vec![1i64, 2i64].into();
672        let err = String::try_from(v.clone()).unwrap_err();
673        match err {
674            MetadataConversionError::WrongType { expected, found } => {
675                assert_eq!(expected, "String");
676                assert_eq!(found, "List");
677            }
678            _ => panic!("unexpected error variant"),
679        }
680        let err_num = i64::try_from(v.clone()).unwrap_err();
681        match err_num {
682            MetadataConversionError::WrongType { expected, found } => {
683                assert_eq!(expected, "Number");
684                assert_eq!(found, "List");
685            }
686            _ => panic!("unexpected error variant"),
687        }
688    }
689
690    #[test]
691    fn try_from_bool_ok() {
692        let v: MetadataValue = true.into();
693        let b: bool = v.clone().try_into().unwrap();
694        assert!(b);
695        let b_ref: bool = (&v).try_into().unwrap();
696        assert!(b_ref);
697    }
698
699    #[test]
700    fn number_out_of_range_for_i64() {
701        let big = MetadataValue::from(u64::MAX);
702        let err = i64::try_from(big).unwrap_err();
703        match err {
704            MetadataConversionError::NumberOutOfRange { expected, .. } => {
705                assert_eq!(expected, "i64");
706            }
707            _ => panic!("expected NumberOutOfRange"),
708        }
709    }
710
711    #[test]
712    fn extract_list_and_map() {
713        let mut child = MetadataMap::new();
714        child.insert("x", 1i64);
715
716        let list_val: MetadataValue = vec![1i64, 2i64].into();
717        let extracted_list: Vec<MetadataValue> = list_val.clone().try_into().unwrap();
718        assert_eq!(extracted_list.len(), 2);
719
720        let map_val: MetadataValue = child.clone().into();
721        let extracted_map: MetadataMap = map_val.try_into().unwrap();
722        assert_eq!(
723            extracted_map
724                .get("x")
725                .unwrap()
726                .as_number()
727                .unwrap()
728                .as_i64(),
729            Some(1)
730        );
731    }
732
733    #[test]
734    fn accessors_work() {
735        let s: MetadataValue = "abc".into();
736        assert_eq!(s.as_str(), Some("abc"));
737        assert!(s.as_number().is_none());
738
739        let n: MetadataValue = 42i64.into();
740        assert_eq!(n.as_number().unwrap().as_i64(), Some(42));
741        assert!(n.as_list().is_none());
742
743        let b: MetadataValue = true.into();
744        assert_eq!(b.as_bool(), Some(true));
745        assert!(b.as_number().is_none());
746
747        let lst: MetadataValue = vec![1i64, 2i64].into();
748        assert_eq!(lst.as_list().unwrap().len(), 2);
749        assert!(lst.as_map().is_none());
750
751        let mut m = MetadataMap::new();
752        m.insert("k", "v");
753        let mv: MetadataValue = m.clone().into();
754        assert_eq!(mv.as_map().unwrap().get("k").unwrap().as_str(), Some("v"));
755    }
756
757    #[test]
758    fn try_from_borrowed_refs() {
759        let v_num: MetadataValue = 7u64.into();
760        let borrowed_u: u64 = (&v_num).try_into().unwrap();
761        assert_eq!(borrowed_u, 7);
762        let borrowed_i: i64 = (&v_num).try_into().unwrap();
763        assert_eq!(borrowed_i, 7);
764
765        let v_str: MetadataValue = "hello".into();
766        let borrowed_str: &str = (&v_str).try_into().unwrap();
767        assert_eq!(borrowed_str, "hello");
768
769        let v_list: MetadataValue = vec!["a", "b"].into();
770        let borrowed_list: Vec<MetadataValue> = (&v_list).try_into().unwrap();
771        assert_eq!(borrowed_list.len(), 2);
772    }
773
774    #[test]
775    fn nan_fallback_is_string_and_fails_number_conversion() {
776        let v_nan: MetadataValue = f64::NAN.into(); // becomes String variant
777        // Expect WrongType because NaN was stored as string
778        let err = f64::try_from(v_nan.clone()).unwrap_err();
779        match err {
780            MetadataConversionError::WrongType { expected, found } => {
781                assert_eq!(expected, "Number");
782                assert_eq!(found, "String");
783            }
784            _ => panic!("unexpected error variant for NaN fallback"),
785        }
786    }
787
788    #[test]
789    fn negative_i64_not_u64() {
790        let neg: MetadataValue = (-5i64).into();
791        let err = u64::try_from(neg).unwrap_err();
792        match err {
793            MetadataConversionError::NumberOutOfRange { expected, .. } => {
794                assert_eq!(expected, "u64");
795            }
796            _ => panic!("expected NumberOutOfRange for negative to u64"),
797        }
798    }
799
800    #[test]
801    fn round_trip_all_variants() {
802        // String
803        let s_mv: MetadataValue = "round".into();
804        assert_eq!(String::try_from(s_mv.clone()).unwrap(), "round");
805        // Number
806        let num_mv: MetadataValue = 123i64.into();
807        assert_eq!(i64::try_from(num_mv.clone()).unwrap(), 123);
808        assert_eq!(u64::try_from(num_mv.clone()).unwrap(), 123u64);
809        // List
810        let list_mv: MetadataValue = vec!["x", "y"].into();
811        let vec_back: Vec<MetadataValue> = list_mv.clone().try_into().unwrap();
812        assert_eq!(vec_back.len(), 2);
813        // Map
814        let mut mm = MetadataMap::new();
815        mm.insert("a", 1i64);
816        let map_mv: MetadataValue = mm.clone().into();
817        let map_back: MetadataMap = map_mv.try_into().unwrap();
818        assert_eq!(
819            map_back
820                .get("a")
821                .unwrap()
822                .as_number()
823                .unwrap()
824                .as_i64()
825                .unwrap(),
826            1
827        );
828    }
829}