agent_skills/
metadata.rs

1//! Metadata type for arbitrary key-value pairs.
2
3use std::collections::HashMap;
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8/// Arbitrary key-value metadata for additional properties.
9///
10/// Keys and values are both strings. This is a simple wrapper around
11/// a `HashMap` with serde support.
12///
13/// # Examples
14///
15/// ```
16/// use agent_skills::Metadata;
17///
18/// let mut metadata = Metadata::new();
19/// metadata.insert("author", "example-org");
20/// metadata.insert("version", "1.0");
21///
22/// assert_eq!(metadata.get("author"), Some("example-org"));
23/// assert_eq!(metadata.len(), 2);
24/// ```
25#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct Metadata(HashMap<String, String>);
28
29impl Metadata {
30    /// Creates a new empty metadata map.
31    #[must_use]
32    pub fn new() -> Self {
33        Self(HashMap::new())
34    }
35
36    /// Creates metadata from an iterator of key-value pairs.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use agent_skills::Metadata;
42    ///
43    /// let metadata = Metadata::from_pairs([
44    ///     ("author", "test"),
45    ///     ("version", "1.0"),
46    /// ]);
47    /// assert_eq!(metadata.len(), 2);
48    /// ```
49    pub fn from_pairs<K, V, I>(iter: I) -> Self
50    where
51        K: Into<String>,
52        V: Into<String>,
53        I: IntoIterator<Item = (K, V)>,
54    {
55        Self(
56            iter.into_iter()
57                .map(|(k, v)| (k.into(), v.into()))
58                .collect(),
59        )
60    }
61
62    /// Inserts a key-value pair.
63    ///
64    /// If the key already exists, the value is replaced.
65    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
66        self.0.insert(key.into(), value.into());
67    }
68
69    /// Gets a value by key.
70    ///
71    /// Returns `None` if the key doesn't exist.
72    #[must_use]
73    pub fn get(&self, key: &str) -> Option<&str> {
74        self.0.get(key).map(String::as_str)
75    }
76
77    /// Returns `true` if the metadata is empty.
78    #[must_use]
79    pub fn is_empty(&self) -> bool {
80        self.0.is_empty()
81    }
82
83    /// Returns the number of entries.
84    #[must_use]
85    pub fn len(&self) -> usize {
86        self.0.len()
87    }
88
89    /// Returns an iterator over key-value pairs.
90    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
91        self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
92    }
93
94    /// Returns `true` if the metadata contains the specified key.
95    #[must_use]
96    pub fn contains_key(&self, key: &str) -> bool {
97        self.0.contains_key(key)
98    }
99
100    /// Returns an iterator over the keys.
101    pub fn keys(&self) -> impl Iterator<Item = &str> {
102        self.0.keys().map(String::as_str)
103    }
104
105    /// Returns an iterator over the values.
106    pub fn values(&self) -> impl Iterator<Item = &str> {
107        self.0.values().map(String::as_str)
108    }
109}
110
111impl fmt::Display for Metadata {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "{{")?;
114        let mut first = true;
115        for (key, value) in &self.0 {
116            if !first {
117                write!(f, ", ")?;
118            }
119            write!(f, "{key}: {value}")?;
120            first = false;
121        }
122        write!(f, "}}")
123    }
124}
125
126impl<K, V> FromIterator<(K, V)> for Metadata
127where
128    K: Into<String>,
129    V: Into<String>,
130{
131    fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
132        Self::from_pairs(iter)
133    }
134}
135
136impl IntoIterator for Metadata {
137    type Item = (String, String);
138    type IntoIter = std::collections::hash_map::IntoIter<String, String>;
139
140    fn into_iter(self) -> Self::IntoIter {
141        self.0.into_iter()
142    }
143}
144
145impl<'a> IntoIterator for &'a Metadata {
146    type Item = (&'a String, &'a String);
147    type IntoIter = std::collections::hash_map::Iter<'a, String, String>;
148
149    fn into_iter(self) -> Self::IntoIter {
150        self.0.iter()
151    }
152}
153
154#[cfg(test)]
155#[allow(clippy::unwrap_used, clippy::expect_used)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn new_creates_empty_metadata() {
161        let metadata = Metadata::new();
162        assert!(metadata.is_empty());
163        assert_eq!(metadata.len(), 0);
164    }
165
166    #[test]
167    fn insert_and_get_work() {
168        let mut metadata = Metadata::new();
169        metadata.insert("key", "value");
170        assert_eq!(metadata.get("key"), Some("value"));
171    }
172
173    #[test]
174    fn get_returns_none_for_missing_key() {
175        let metadata = Metadata::new();
176        assert_eq!(metadata.get("missing"), None);
177    }
178
179    #[test]
180    fn from_iter_creates_populated_metadata() {
181        let metadata = Metadata::from_pairs([("author", "test"), ("version", "1.0")]);
182        assert_eq!(metadata.len(), 2);
183        assert_eq!(metadata.get("author"), Some("test"));
184        assert_eq!(metadata.get("version"), Some("1.0"));
185    }
186
187    #[test]
188    fn contains_key_works() {
189        let metadata = Metadata::from_pairs([("key", "value")]);
190        assert!(metadata.contains_key("key"));
191        assert!(!metadata.contains_key("missing"));
192    }
193
194    #[test]
195    fn iter_works() {
196        let metadata = Metadata::from_pairs([("a", "1"), ("b", "2")]);
197        let pairs: Vec<_> = metadata.iter().collect();
198        assert_eq!(pairs.len(), 2);
199    }
200
201    #[test]
202    fn keys_works() {
203        let metadata = Metadata::from_pairs([("a", "1"), ("b", "2")]);
204        let keys: Vec<_> = metadata.keys().collect();
205        assert_eq!(keys.len(), 2);
206    }
207
208    #[test]
209    fn values_works() {
210        let metadata = Metadata::from_pairs([("a", "1"), ("b", "2")]);
211        let values: Vec<_> = metadata.values().collect();
212        assert_eq!(values.len(), 2);
213    }
214
215    #[test]
216    fn display_works() {
217        let metadata = Metadata::from_pairs([("key", "value")]);
218        let display = format!("{metadata}");
219        assert!(display.contains("key"));
220        assert!(display.contains("value"));
221    }
222
223    #[test]
224    fn collect_works() {
225        let pairs = vec![("a", "1"), ("b", "2")];
226        let metadata: Metadata = pairs.into_iter().collect();
227        assert_eq!(metadata.len(), 2);
228    }
229
230    #[test]
231    fn into_iter_works() {
232        let metadata = Metadata::from_pairs([("a", "1")]);
233        let pairs: Vec<_> = metadata.into_iter().collect();
234        assert_eq!(pairs.len(), 1);
235    }
236
237    #[test]
238    fn ref_into_iter_works() {
239        let metadata = Metadata::from_pairs([("a", "1")]);
240        let pairs: Vec<_> = (&metadata).into_iter().collect();
241        assert_eq!(pairs.len(), 1);
242    }
243}