Skip to main content

eventuary_core/
topic.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::{Error, Result};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(try_from = "String", into = "String")]
9pub struct Topic(String);
10
11impl Topic {
12    pub fn new(s: impl Into<String>) -> Result<Self> {
13        let s = s.into();
14        if s.is_empty() {
15            return Err(Error::InvalidTopic("must not be empty".into()));
16        }
17        for part in s.split('.') {
18            if part.is_empty() {
19                return Err(Error::InvalidTopic("empty segment".into()));
20            }
21            if !part
22                .chars()
23                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
24            {
25                return Err(Error::InvalidTopic(format!("invalid segment: {part}")));
26            }
27        }
28        Ok(Self(s))
29    }
30
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36impl fmt::Display for Topic {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}", self.0)
39    }
40}
41
42impl TryFrom<String> for Topic {
43    type Error = Error;
44    fn try_from(s: String) -> Result<Self> {
45        Self::new(s)
46    }
47}
48
49impl From<Topic> for String {
50    fn from(t: Topic) -> Self {
51        t.0
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn valid_topics() {
61        assert!(Topic::new("task.created").is_ok());
62        assert!(Topic::new("agent.roles_changed").is_ok());
63        assert!(Topic::new("memory.updated").is_ok());
64    }
65
66    #[test]
67    fn invalid_topics() {
68        assert!(Topic::new("").is_err());
69        assert!(Topic::new("task..created").is_err());
70        assert!(Topic::new("Task.Created").is_err());
71    }
72}