eventuary-core 0.1.0

Core event model and async IO traits for eventuary
Documentation
use std::fmt;

use serde::{Deserialize, Serialize};

use crate::error::{Error, Result};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ConsumerGroupId(String);

impl ConsumerGroupId {
    pub fn new(s: impl Into<String>) -> Result<Self> {
        let s = s.into();
        if s.is_empty() {
            return Err(Error::InvalidConsumerGroupId("must not be empty".into()));
        }
        if s.len() > 64 {
            return Err(Error::InvalidConsumerGroupId("max 64 chars".into()));
        }
        if !s
            .chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
        {
            return Err(Error::InvalidConsumerGroupId(format!("invalid: {s}")));
        }
        Ok(Self(s))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for ConsumerGroupId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl TryFrom<String> for ConsumerGroupId {
    type Error = Error;
    fn try_from(s: String) -> Result<Self> {
        Self::new(s)
    }
}

impl From<ConsumerGroupId> for String {
    fn from(g: ConsumerGroupId) -> Self {
        g.0
    }
}

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

    #[test]
    fn valid() {
        assert!(ConsumerGroupId::new("heartbeat-monitor").is_ok());
        assert!(ConsumerGroupId::new("group_1").is_ok());
        assert!(ConsumerGroupId::new("g").is_ok());
    }

    #[test]
    fn invalid_empty() {
        assert!(ConsumerGroupId::new("").is_err());
    }

    #[test]
    fn invalid_too_long() {
        let s = "a".repeat(65);
        assert!(ConsumerGroupId::new(s).is_err());
    }

    #[test]
    fn invalid_charset() {
        assert!(ConsumerGroupId::new("Heartbeat").is_err());
        assert!(ConsumerGroupId::new("group.1").is_err());
        assert!(ConsumerGroupId::new("group/1").is_err());
    }

    #[test]
    fn boundary_64_chars() {
        let s = "a".repeat(64);
        assert!(ConsumerGroupId::new(s).is_ok());
    }

    #[test]
    fn try_from_string_roundtrip() {
        let g = ConsumerGroupId::new("my-group").unwrap();
        let s: String = g.clone().into();
        let g2 = ConsumerGroupId::try_from(s).unwrap();
        assert_eq!(g, g2);
    }
}