Skip to main content

eventuary_core/
metadata.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Result;
6use crate::field_map::FieldMap;
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize)]
9#[serde(transparent)]
10pub struct Metadata(FieldMap<String>);
11
12impl Metadata {
13    pub fn new() -> Self {
14        Self(FieldMap::new())
15    }
16
17    pub fn with(self, key: impl Into<String>, value: impl Into<String>) -> Result<Self> {
18        Ok(Self(self.0.with(key, value.into())?))
19    }
20
21    pub fn from_map(map: BTreeMap<String, String>) -> Result<Self> {
22        Ok(Self(FieldMap::try_from_map(map)?))
23    }
24
25    pub fn get(&self, key: &str) -> Option<&str> {
26        self.0.get(key).map(|s| s.as_str())
27    }
28
29    pub fn has(&self, key: &str) -> bool {
30        self.0.has(key)
31    }
32
33    pub fn len(&self) -> usize {
34        self.0.len()
35    }
36
37    pub fn is_empty(&self) -> bool {
38        self.0.is_empty()
39    }
40
41    pub fn as_map(&self) -> &BTreeMap<String, String> {
42        self.0.as_map()
43    }
44
45    pub fn into_map(self) -> BTreeMap<String, String> {
46        self.0.into_map()
47    }
48}
49
50impl TryFrom<BTreeMap<String, String>> for Metadata {
51    type Error = crate::error::Error;
52
53    fn try_from(map: BTreeMap<String, String>) -> Result<Self> {
54        Self::from_map(map)
55    }
56}
57
58impl TryFrom<HashMap<String, String>> for Metadata {
59    type Error = crate::error::Error;
60
61    fn try_from(map: HashMap<String, String>) -> Result<Self> {
62        Self::from_map(map.into_iter().collect())
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::error::Error;
70
71    #[test]
72    fn with_inserts_pair() {
73        let m = Metadata::new().with("k", "v").unwrap();
74        assert_eq!(m.get("k"), Some("v"));
75        assert!(m.has("k"));
76        assert_eq!(m.len(), 1);
77    }
78
79    #[test]
80    fn with_rejects_empty_key() {
81        let res = Metadata::new().with("", "v");
82        assert!(matches!(res, Err(Error::InvalidMetadataKey(_))));
83    }
84
85    #[test]
86    fn with_rejects_whitespace_padded_key() {
87        assert!(matches!(
88            Metadata::new().with(" key", "v"),
89            Err(Error::InvalidMetadataKey(_))
90        ));
91        assert!(matches!(
92            Metadata::new().with("key ", "v"),
93            Err(Error::InvalidMetadataKey(_))
94        ));
95    }
96
97    #[test]
98    fn with_rejects_newlines_in_key() {
99        assert!(matches!(
100            Metadata::new().with("k\nv", "v"),
101            Err(Error::InvalidMetadataKey(_))
102        ));
103    }
104
105    #[test]
106    fn from_map_validates_keys() {
107        let mut valid = BTreeMap::new();
108        valid.insert("source".to_owned(), "billing".to_owned());
109        let m = Metadata::from_map(valid).unwrap();
110        assert_eq!(m.get("source"), Some("billing"));
111
112        let mut invalid = BTreeMap::new();
113        invalid.insert(String::new(), "v".to_owned());
114        assert!(matches!(
115            Metadata::from_map(invalid),
116            Err(Error::InvalidMetadataKey(_))
117        ));
118    }
119
120    #[test]
121    fn try_from_hashmap_validates_keys() {
122        let mut invalid = HashMap::new();
123        invalid.insert(String::new(), "v".to_owned());
124        assert!(matches!(
125            Metadata::try_from(invalid),
126            Err(Error::InvalidMetadataKey(_))
127        ));
128    }
129
130    #[test]
131    fn serializes_as_plain_map() {
132        let m = Metadata::new().with("source", "billing").unwrap();
133        let json = serde_json::to_string(&m).unwrap();
134        assert_eq!(json, r#"{"source":"billing"}"#);
135    }
136
137    #[test]
138    fn deserializes_from_plain_map() {
139        let m: Metadata = serde_json::from_str(r#"{"source":"billing"}"#).unwrap();
140        assert_eq!(m.get("source"), Some("billing"));
141    }
142}