eventuary-core 0.1.0

Core event model and async IO traits for eventuary
Documentation
use std::collections::{BTreeMap, HashMap};

use serde::{Deserialize, Serialize};

use crate::error::Result;
use crate::field_map::FieldMap;

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Metadata(FieldMap<String>);

impl Metadata {
    pub fn new() -> Self {
        Self(FieldMap::new())
    }

    pub fn with(self, key: impl Into<String>, value: impl Into<String>) -> Result<Self> {
        Ok(Self(self.0.with(key, value.into())?))
    }

    pub fn from_map(map: BTreeMap<String, String>) -> Result<Self> {
        Ok(Self(FieldMap::try_from_map(map)?))
    }

    pub fn get(&self, key: &str) -> Option<&str> {
        self.0.get(key).map(|s| s.as_str())
    }

    pub fn has(&self, key: &str) -> bool {
        self.0.has(key)
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn as_map(&self) -> &BTreeMap<String, String> {
        self.0.as_map()
    }

    pub fn into_map(self) -> BTreeMap<String, String> {
        self.0.into_map()
    }
}

impl TryFrom<BTreeMap<String, String>> for Metadata {
    type Error = crate::error::Error;

    fn try_from(map: BTreeMap<String, String>) -> Result<Self> {
        Self::from_map(map)
    }
}

impl TryFrom<HashMap<String, String>> for Metadata {
    type Error = crate::error::Error;

    fn try_from(map: HashMap<String, String>) -> Result<Self> {
        Self::from_map(map.into_iter().collect())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::Error;

    #[test]
    fn with_inserts_pair() {
        let m = Metadata::new().with("k", "v").unwrap();
        assert_eq!(m.get("k"), Some("v"));
        assert!(m.has("k"));
        assert_eq!(m.len(), 1);
    }

    #[test]
    fn with_rejects_empty_key() {
        let res = Metadata::new().with("", "v");
        assert!(matches!(res, Err(Error::InvalidMetadataKey(_))));
    }

    #[test]
    fn with_rejects_whitespace_padded_key() {
        assert!(matches!(
            Metadata::new().with(" key", "v"),
            Err(Error::InvalidMetadataKey(_))
        ));
        assert!(matches!(
            Metadata::new().with("key ", "v"),
            Err(Error::InvalidMetadataKey(_))
        ));
    }

    #[test]
    fn with_rejects_newlines_in_key() {
        assert!(matches!(
            Metadata::new().with("k\nv", "v"),
            Err(Error::InvalidMetadataKey(_))
        ));
    }

    #[test]
    fn from_map_validates_keys() {
        let mut valid = BTreeMap::new();
        valid.insert("source".to_owned(), "billing".to_owned());
        let m = Metadata::from_map(valid).unwrap();
        assert_eq!(m.get("source"), Some("billing"));

        let mut invalid = BTreeMap::new();
        invalid.insert(String::new(), "v".to_owned());
        assert!(matches!(
            Metadata::from_map(invalid),
            Err(Error::InvalidMetadataKey(_))
        ));
    }

    #[test]
    fn try_from_hashmap_validates_keys() {
        let mut invalid = HashMap::new();
        invalid.insert(String::new(), "v".to_owned());
        assert!(matches!(
            Metadata::try_from(invalid),
            Err(Error::InvalidMetadataKey(_))
        ));
    }

    #[test]
    fn serializes_as_plain_map() {
        let m = Metadata::new().with("source", "billing").unwrap();
        let json = serde_json::to_string(&m).unwrap();
        assert_eq!(json, r#"{"source":"billing"}"#);
    }

    #[test]
    fn deserializes_from_plain_map() {
        let m: Metadata = serde_json::from_str(r#"{"source":"billing"}"#).unwrap();
        assert_eq!(m.get("source"), Some("billing"));
    }
}