1use std::collections::HashMap;
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct Metadata(HashMap<String, String>);
28
29impl Metadata {
30 #[must_use]
32 pub fn new() -> Self {
33 Self(HashMap::new())
34 }
35
36 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 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
66 self.0.insert(key.into(), value.into());
67 }
68
69 #[must_use]
73 pub fn get(&self, key: &str) -> Option<&str> {
74 self.0.get(key).map(String::as_str)
75 }
76
77 #[must_use]
79 pub fn is_empty(&self) -> bool {
80 self.0.is_empty()
81 }
82
83 #[must_use]
85 pub fn len(&self) -> usize {
86 self.0.len()
87 }
88
89 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 #[must_use]
96 pub fn contains_key(&self, key: &str) -> bool {
97 self.0.contains_key(key)
98 }
99
100 pub fn keys(&self) -> impl Iterator<Item = &str> {
102 self.0.keys().map(String::as_str)
103 }
104
105 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}