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 Topic(String);

impl Topic {
    pub fn new(s: impl Into<String>) -> Result<Self> {
        let s = s.into();
        if s.is_empty() {
            return Err(Error::InvalidTopic("must not be empty".into()));
        }
        for part in s.split('.') {
            if part.is_empty() {
                return Err(Error::InvalidTopic("empty segment".into()));
            }
            if !part
                .chars()
                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
            {
                return Err(Error::InvalidTopic(format!("invalid segment: {part}")));
            }
        }
        Ok(Self(s))
    }

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

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

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

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

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

    #[test]
    fn valid_topics() {
        assert!(Topic::new("task.created").is_ok());
        assert!(Topic::new("agent.roles_changed").is_ok());
        assert!(Topic::new("memory.updated").is_ok());
    }

    #[test]
    fn invalid_topics() {
        assert!(Topic::new("").is_err());
        assert!(Topic::new("task..created").is_err());
        assert!(Topic::new("Task.Created").is_err());
    }
}