1use std::fmt;
2use std::sync::Arc;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::error::TopiqError;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct Subject(Arc<str>);
17
18impl Serialize for Subject {
19 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
20 self.0.serialize(serializer)
21 }
22}
23
24impl<'de> Deserialize<'de> for Subject {
25 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
26 let s = String::deserialize(deserializer)?;
27 Subject::new(&s).map_err(serde::de::Error::custom)
28 }
29}
30
31impl Subject {
32 pub fn new(raw: &str) -> crate::Result<Self> {
33 validate_subject(raw)?;
34 Ok(Self(Arc::from(raw)))
35 }
36
37 pub fn as_str(&self) -> &str {
38 &self.0
39 }
40
41 pub fn tokens(&self) -> impl Iterator<Item = &str> {
42 self.0.split('.')
43 }
44
45 pub fn is_wildcard(&self) -> bool {
46 self.0.contains('*') || self.0.contains('>')
47 }
48
49 pub fn token_count(&self) -> usize {
50 self.0.split('.').count()
51 }
52}
53
54impl fmt::Display for Subject {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str(&self.0)
57 }
58}
59
60impl AsRef<str> for Subject {
61 fn as_ref(&self) -> &str {
62 &self.0
63 }
64}
65
66const MAX_SUBJECT_LEN: usize = 256;
68
69fn validate_subject(raw: &str) -> crate::Result<()> {
70 if raw.is_empty() {
71 return Err(TopiqError::InvalidSubject {
72 reason: "subject cannot be empty".into(),
73 });
74 }
75
76 if raw.len() > MAX_SUBJECT_LEN {
77 return Err(TopiqError::InvalidSubject {
78 reason: format!(
79 "subject exceeds maximum length of {} bytes (got {})",
80 MAX_SUBJECT_LEN,
81 raw.len()
82 ),
83 });
84 }
85
86 if raw.starts_with('.') || raw.ends_with('.') {
87 return Err(TopiqError::InvalidSubject {
88 reason: "subject cannot start or end with '.'".into(),
89 });
90 }
91
92 if raw.contains("..") {
93 return Err(TopiqError::InvalidSubject {
94 reason: "subject cannot contain empty tokens (double dots)".into(),
95 });
96 }
97
98 let tokens: Vec<&str> = raw.split('.').collect();
99 for (i, token) in tokens.iter().enumerate() {
100 if token.is_empty() {
101 return Err(TopiqError::InvalidSubject {
102 reason: "subject contains an empty token".into(),
103 });
104 }
105
106 if *token == ">" && i != tokens.len() - 1 {
107 return Err(TopiqError::InvalidSubject {
108 reason: "'>' wildcard must be the last token".into(),
109 });
110 }
111
112 if (token.contains('*') || token.contains('>')) && token.len() > 1 {
113 return Err(TopiqError::InvalidSubject {
114 reason: format!("wildcard token '{}' must stand alone", token),
115 });
116 }
117 }
118
119 Ok(())
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn valid_simple_subject() {
128 assert!(Subject::new("sensors.temp.room1").is_ok());
129 }
130
131 #[test]
132 fn valid_single_token() {
133 assert!(Subject::new("hello").is_ok());
134 }
135
136 #[test]
137 fn valid_wildcard_star() {
138 assert!(Subject::new("sensors.*.room1").is_ok());
139 }
140
141 #[test]
142 fn valid_wildcard_gt() {
143 assert!(Subject::new("sensors.>").is_ok());
144 }
145
146 #[test]
147 fn valid_star_only() {
148 assert!(Subject::new("*").is_ok());
149 }
150
151 #[test]
152 fn valid_gt_only() {
153 assert!(Subject::new(">").is_ok());
154 }
155
156 #[test]
157 fn invalid_empty() {
158 assert!(Subject::new("").is_err());
159 }
160
161 #[test]
162 fn invalid_double_dot() {
163 assert!(Subject::new("sensors..temp").is_err());
164 }
165
166 #[test]
167 fn invalid_leading_dot() {
168 assert!(Subject::new(".sensors").is_err());
169 }
170
171 #[test]
172 fn invalid_trailing_dot() {
173 assert!(Subject::new("sensors.").is_err());
174 }
175
176 #[test]
177 fn invalid_gt_not_last() {
178 assert!(Subject::new("sensors.>.temp").is_err());
179 }
180
181 #[test]
182 fn invalid_mixed_wildcard() {
183 assert!(Subject::new("sensors.te*").is_err());
184 }
185
186 #[test]
187 fn tokens_returns_segments() {
188 let s = Subject::new("a.b.c").unwrap();
189 let tokens: Vec<&str> = s.tokens().collect();
190 assert_eq!(tokens, vec!["a", "b", "c"]);
191 }
192
193 #[test]
194 fn is_wildcard_detection() {
195 assert!(!Subject::new("a.b").unwrap().is_wildcard());
196 assert!(Subject::new("a.*").unwrap().is_wildcard());
197 assert!(Subject::new("a.>").unwrap().is_wildcard());
198 }
199
200 #[test]
201 fn display_roundtrip() {
202 let s = Subject::new("foo.bar").unwrap();
203 assert_eq!(s.to_string(), "foo.bar");
204 }
205
206 #[test]
207 fn serde_roundtrip() {
208 let s = Subject::new("a.b.c").unwrap();
209 let encoded = rmp_serde::to_vec(&s).unwrap();
210 let decoded: Subject = rmp_serde::from_slice(&encoded).unwrap();
211 assert_eq!(s, decoded);
212 }
213}