Skip to main content

dendryform_core/
metadata.rs

1//! Extensibility metadata type.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7/// An extensibility escape hatch for user-defined key-value pairs.
8///
9/// Wraps a `HashMap<String, String>` with explicit accessor methods.
10/// Does **not** implement `Deref<Target = HashMap>` (AP-15).
11///
12/// # Examples
13///
14/// ```
15/// use dendryform_core::Metadata;
16///
17/// let mut meta = Metadata::new();
18/// meta.insert("owner", "platform-team");
19/// assert_eq!(meta.get("owner"), Some("platform-team"));
20/// ```
21#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct Metadata(HashMap<String, String>);
24
25impl Metadata {
26    /// Creates an empty metadata map.
27    pub fn new() -> Self {
28        Self(HashMap::new())
29    }
30
31    /// Returns the value for the given key, if present.
32    pub fn get(&self, key: &str) -> Option<&str> {
33        self.0.get(key).map(|v| v.as_str())
34    }
35
36    /// Inserts a key-value pair, returning the previous value if any.
37    pub fn insert(&mut self, key: &str, value: &str) -> Option<String> {
38        self.0.insert(key.to_owned(), value.to_owned())
39    }
40
41    /// Returns an iterator over key-value pairs.
42    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
43        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
44    }
45
46    /// Returns `true` if the metadata map is empty.
47    pub fn is_empty(&self) -> bool {
48        self.0.is_empty()
49    }
50
51    /// Returns the number of entries.
52    pub fn len(&self) -> usize {
53        self.0.len()
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_new_is_empty() {
63        let meta = Metadata::new();
64        assert!(meta.is_empty());
65        assert_eq!(meta.len(), 0);
66    }
67
68    #[test]
69    fn test_insert_and_get() {
70        let mut meta = Metadata::new();
71        meta.insert("team", "platform");
72        assert_eq!(meta.get("team"), Some("platform"));
73        assert_eq!(meta.get("missing"), None);
74        assert!(!meta.is_empty());
75        assert_eq!(meta.len(), 1);
76    }
77
78    #[test]
79    fn test_serde_round_trip() {
80        let mut meta = Metadata::new();
81        meta.insert("owner", "alice");
82        meta.insert("cost-center", "eng");
83
84        let json = serde_json::to_string(&meta).unwrap();
85        let deserialized: Metadata = serde_json::from_str(&json).unwrap();
86        assert_eq!(meta, deserialized);
87    }
88
89    #[test]
90    fn test_iter() {
91        let mut meta = Metadata::new();
92        meta.insert("a", "1");
93        meta.insert("b", "2");
94        let pairs: Vec<_> = meta.iter().collect();
95        assert_eq!(pairs.len(), 2);
96    }
97
98    #[test]
99    fn test_insert_returns_previous() {
100        let mut meta = Metadata::new();
101        let prev = meta.insert("key", "value1");
102        assert!(prev.is_none());
103        let prev = meta.insert("key", "value2");
104        assert_eq!(prev, Some("value1".to_owned()));
105        assert_eq!(meta.get("key"), Some("value2"));
106    }
107
108    #[test]
109    fn test_default() {
110        let meta = Metadata::default();
111        assert!(meta.is_empty());
112        assert_eq!(meta.len(), 0);
113    }
114
115    #[test]
116    fn test_debug() {
117        let mut meta = Metadata::new();
118        meta.insert("k", "v");
119        let debug = format!("{meta:?}");
120        assert!(debug.contains("Metadata"));
121    }
122
123    #[test]
124    fn test_clone_eq() {
125        let mut meta = Metadata::new();
126        meta.insert("x", "y");
127        let cloned = meta.clone();
128        assert_eq!(meta, cloned);
129    }
130}