emergent_client/types/
message_type.rs1use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct MessageType(String);
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum InvalidMessageType {
24 Empty,
26 InvalidCharacters { value: String },
28 InvalidStructure { value: String, reason: &'static str },
30}
31
32impl fmt::Display for InvalidMessageType {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 Self::Empty => write!(f, "message type cannot be empty"),
36 Self::InvalidCharacters { value } => {
37 write!(f, "message type '{value}' contains invalid characters")
38 }
39 Self::InvalidStructure { value, reason } => {
40 write!(f, "message type '{value}' is invalid: {reason}")
41 }
42 }
43 }
44}
45
46impl std::error::Error for InvalidMessageType {}
47
48impl MessageType {
49 pub fn new(value: impl Into<String>) -> Result<Self, InvalidMessageType> {
55 let value = value.into();
56
57 if value.is_empty() {
58 return Err(InvalidMessageType::Empty);
59 }
60
61 if value.starts_with('.') || value.ends_with('.') {
62 return Err(InvalidMessageType::InvalidStructure {
63 value,
64 reason: "cannot start or end with dot",
65 });
66 }
67
68 if value.contains("..") {
69 return Err(InvalidMessageType::InvalidStructure {
70 value,
71 reason: "cannot contain consecutive dots",
72 });
73 }
74
75 if !value.chars().all(|c| {
76 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '.' || c == '-' || c == '_'
77 }) {
78 return Err(InvalidMessageType::InvalidCharacters { value });
79 }
80
81 Ok(Self(value))
82 }
83
84 #[must_use]
86 pub fn as_str(&self) -> &str {
87 &self.0
88 }
89
90 #[must_use]
94 pub fn parts(&self) -> Vec<&str> {
95 self.0.split('.').collect()
96 }
97
98 #[must_use]
102 pub fn category(&self) -> Option<&str> {
103 self.0.split('.').next()
104 }
105
106 #[must_use]
114 pub fn matches_pattern(&self, pattern: &str) -> bool {
115 if let Some(prefix) = pattern.strip_suffix(".*") {
116 self.0.starts_with(prefix)
117 && self.0.len() > prefix.len()
118 && self.0.as_bytes().get(prefix.len()) == Some(&b'.')
119 } else {
120 self.0 == pattern
121 }
122 }
123}
124
125impl fmt::Display for MessageType {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(f, "{}", self.0)
128 }
129}
130
131impl FromStr for MessageType {
132 type Err = InvalidMessageType;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 Self::new(s)
136 }
137}
138
139impl AsRef<str> for MessageType {
140 fn as_ref(&self) -> &str {
141 &self.0
142 }
143}
144
145impl Serialize for MessageType {
146 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147 where
148 S: Serializer,
149 {
150 self.0.serialize(serializer)
151 }
152}
153
154impl<'de> Deserialize<'de> for MessageType {
155 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
156 where
157 D: Deserializer<'de>,
158 {
159 let s = String::deserialize(deserializer)?;
160 Self::new(s).map_err(serde::de::Error::custom)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn valid_message_types() {
170 assert!(MessageType::new("timer.tick").is_ok());
171 assert!(MessageType::new("user.created").is_ok());
172 assert!(MessageType::new("system.shutdown").is_ok());
173 assert!(MessageType::new("event").is_ok());
174 assert!(MessageType::new("namespace.sub.event").is_ok());
175 assert!(MessageType::new("with-hyphen").is_ok());
176 assert!(MessageType::new("with_underscore").is_ok());
177 assert!(MessageType::new("with123numbers").is_ok());
178 }
179
180 #[test]
181 fn empty_is_invalid() {
182 let result = MessageType::new("");
183 assert!(matches!(result, Err(InvalidMessageType::Empty)));
184 }
185
186 #[test]
187 fn starting_with_dot_is_invalid() {
188 let result = MessageType::new(".invalid");
189 assert!(matches!(
190 result,
191 Err(InvalidMessageType::InvalidStructure { .. })
192 ));
193 }
194
195 #[test]
196 fn ending_with_dot_is_invalid() {
197 let result = MessageType::new("invalid.");
198 assert!(matches!(
199 result,
200 Err(InvalidMessageType::InvalidStructure { .. })
201 ));
202 }
203
204 #[test]
205 fn consecutive_dots_invalid() {
206 let result = MessageType::new("in..valid");
207 assert!(matches!(
208 result,
209 Err(InvalidMessageType::InvalidStructure { .. })
210 ));
211 }
212
213 #[test]
214 fn uppercase_is_invalid() {
215 let result = MessageType::new("Invalid");
216 assert!(matches!(
217 result,
218 Err(InvalidMessageType::InvalidCharacters { .. })
219 ));
220 }
221
222 #[test]
223 fn parts_extraction() -> Result<(), InvalidMessageType> {
224 let msg_type = MessageType::new("user.created")?;
225 assert_eq!(msg_type.parts(), vec!["user", "created"]);
226 Ok(())
227 }
228
229 #[test]
230 fn category_extraction() -> Result<(), InvalidMessageType> {
231 let msg_type = MessageType::new("user.created")?;
232 assert_eq!(msg_type.category(), Some("user"));
233 Ok(())
234 }
235
236 #[test]
237 fn matches_pattern_exact() -> Result<(), InvalidMessageType> {
238 let msg_type = MessageType::new("timer.tick")?;
239 assert!(msg_type.matches_pattern("timer.tick"));
240 assert!(!msg_type.matches_pattern("timer.tock"));
241 Ok(())
242 }
243
244 #[test]
245 fn matches_pattern_wildcard() -> Result<(), InvalidMessageType> {
246 let msg_type = MessageType::new("system.started.timer")?;
247 assert!(msg_type.matches_pattern("system.started.*"));
248 assert!(msg_type.matches_pattern("system.*"));
249 assert!(!msg_type.matches_pattern("user.*"));
250 Ok(())
251 }
252
253 #[test]
254 fn serde_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
255 let msg_type = MessageType::new("timer.tick")?;
256 let json = serde_json::to_string(&msg_type)?;
257 let restored: MessageType = serde_json::from_str(&json)?;
258 assert_eq!(msg_type, restored);
259 Ok(())
260 }
261
262 #[test]
263 fn from_str_works() -> Result<(), InvalidMessageType> {
264 let msg_type: MessageType = "timer.tick".parse()?;
265 assert_eq!(msg_type.as_str(), "timer.tick");
266 Ok(())
267 }
268}