evidentsource_core/domain/
identifiers.rs1use once_cell::sync::Lazy;
7use regex::Regex;
8use std::fmt::Display;
9
10use super::error::IdentifierError;
11
12pub const SAFE_DELIMITER: char = '\u{001F}';
14
15pub const SEQUENCE_KEY: &str = "sequence";
17
18pub const RECORDEDTIME_KEY: &str = "recordedtime";
20
21pub const NAME_PATTERN_STR: &str = r"^[a-zA-Z][a-zA-Z0-9\-_.]{1,127}$";
23
24#[allow(clippy::declare_interior_mutable_const)]
26pub static NAME_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(NAME_PATTERN_STR).unwrap());
27
28fn non_blank_len_lt_256_no_safe_delimiter(value: &str) -> bool {
30 !value.trim().is_empty() && value.len() < 256 && !value.contains(SAFE_DELIMITER)
31}
32
33#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
39pub struct DatabaseName(String);
40
41impl Display for DatabaseName {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(&self.0)
44 }
45}
46
47impl DatabaseName {
48 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
50 let s = value.into();
51 if NAME_PATTERN.is_match(&s) {
52 Ok(DatabaseName(s))
53 } else {
54 Err(IdentifierError::DatabaseName(s))
55 }
56 }
57
58 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62}
63
64#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
70pub struct StreamName(String);
71
72impl std::fmt::Debug for StreamName {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 let truncated = if self.0.len() > 8 {
75 format!("{}...", &self.0[..8])
76 } else {
77 self.0.clone()
78 };
79 f.debug_tuple("StreamName").field(&truncated).finish()
80 }
81}
82
83impl Display for StreamName {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.write_str(&self.0)
86 }
87}
88
89impl StreamName {
90 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
92 let s = value.into();
93 if NAME_PATTERN.is_match(&s) {
94 Ok(StreamName(s))
95 } else {
96 Err(IdentifierError::StreamName(Some(s)))
97 }
98 }
99
100 pub fn from_source(url: &str) -> Result<Self, IdentifierError> {
102 let source = url
103 .split('/')
104 .next_back()
105 .ok_or_else(|| IdentifierError::StreamName(Some(url.to_string())))?;
106 Self::new(source)
107 }
108
109 pub fn as_str(&self) -> &str {
111 &self.0
112 }
113}
114
115#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
121pub struct EventId(String);
122
123impl std::fmt::Debug for EventId {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 let truncated = if self.0.len() > 8 {
126 format!("{}...", &self.0[..8])
127 } else {
128 self.0.clone()
129 };
130 f.debug_tuple("EventId").field(&truncated).finish()
131 }
132}
133
134impl Display for EventId {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.write_str(&self.0)
137 }
138}
139
140impl EventId {
141 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
143 let s = value.into();
144 if non_blank_len_lt_256_no_safe_delimiter(&s) {
145 Ok(EventId(s))
146 } else {
147 Err(IdentifierError::EventId(s))
148 }
149 }
150
151 pub fn as_str(&self) -> &str {
153 &self.0
154 }
155}
156
157#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
163pub struct EventType(String);
164
165impl std::fmt::Debug for EventType {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 let truncated = if self.0.len() > 16 {
168 format!("{}...", &self.0[..16])
169 } else {
170 self.0.clone()
171 };
172 f.debug_tuple("EventType").field(&truncated).finish()
173 }
174}
175
176impl Display for EventType {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.write_str(&self.0)
179 }
180}
181
182impl EventType {
183 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
185 let s = value.into();
186 if non_blank_len_lt_256_no_safe_delimiter(&s) {
187 Ok(EventType(s))
188 } else {
189 Err(IdentifierError::EventType(Some(s)))
190 }
191 }
192
193 pub fn as_str(&self) -> &str {
195 &self.0
196 }
197}
198
199#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
205pub struct EventSubject(String);
206
207impl std::fmt::Debug for EventSubject {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 let truncated = if self.0.len() > 8 {
210 format!("{}...", &self.0[..8])
211 } else {
212 self.0.clone()
213 };
214 f.debug_tuple("EventSubject").field(&truncated).finish()
215 }
216}
217
218impl Display for EventSubject {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.write_str(&self.0)
221 }
222}
223
224impl EventSubject {
225 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
227 let s = value.into();
228 if non_blank_len_lt_256_no_safe_delimiter(&s) {
229 Ok(EventSubject(s))
230 } else {
231 Err(IdentifierError::EventSubject(s))
232 }
233 }
234
235 pub fn from_option<T: AsRef<str>>(value: Option<T>) -> Result<Option<Self>, IdentifierError> {
237 match value {
238 Some(v) => Self::new(v.as_ref()).map(Some),
239 None => Ok(None),
240 }
241 }
242
243 pub fn as_str(&self) -> &str {
245 &self.0
246 }
247}
248
249#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
255pub struct StateViewName(String);
256
257impl std::fmt::Debug for StateViewName {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 let truncated = if self.0.len() > 16 {
260 format!("{}...", &self.0[..16])
261 } else {
262 self.0.clone()
263 };
264 f.debug_tuple("StateViewName").field(&truncated).finish()
265 }
266}
267
268impl Display for StateViewName {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 f.write_str(&self.0)
271 }
272}
273
274impl StateViewName {
275 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
277 let s = value.into();
278 if NAME_PATTERN.is_match(&s) {
279 Ok(StateViewName(s))
280 } else {
281 Err(IdentifierError::StateViewName(s))
282 }
283 }
284
285 pub fn as_str(&self) -> &str {
287 &self.0
288 }
289}
290
291#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
297pub struct StateChangeName(String);
298
299impl std::fmt::Debug for StateChangeName {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 let truncated = if self.0.len() > 16 {
302 format!("{}...", &self.0[..16])
303 } else {
304 self.0.clone()
305 };
306 f.debug_tuple("StateChangeName").field(&truncated).finish()
307 }
308}
309
310impl Display for StateChangeName {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.write_str(&self.0)
313 }
314}
315
316impl StateChangeName {
317 pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
319 let s = value.into();
320 if NAME_PATTERN.is_match(&s) {
321 Ok(StateChangeName(s))
322 } else {
323 Err(IdentifierError::StateChangeName(s))
324 }
325 }
326
327 pub fn as_str(&self) -> &str {
329 &self.0
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_database_name_valid() {
339 assert!(DatabaseName::new("my-database").is_ok());
340 assert!(DatabaseName::new("MyDB_123").is_ok());
341 assert!(DatabaseName::new("a1").is_ok());
342 }
343
344 #[test]
345 fn test_database_name_invalid() {
346 assert!(DatabaseName::new("").is_err());
347 assert!(DatabaseName::new("1invalid").is_err()); assert!(DatabaseName::new("a").is_err()); }
350
351 #[test]
352 fn test_stream_name_from_source() {
353 let name = StreamName::from_source("http://example.com/streams/my-stream").unwrap();
354 assert_eq!(name.as_str(), "my-stream");
355 }
356
357 #[test]
358 fn test_event_id_valid() {
359 assert!(EventId::new("evt-123").is_ok());
360 assert!(EventId::new("a").is_ok());
361 }
362
363 #[test]
364 fn test_event_id_invalid() {
365 assert!(EventId::new("").is_err());
366 assert!(EventId::new(" ").is_err());
367 }
368
369 #[test]
370 fn test_event_subject_optional() {
371 assert!(EventSubject::from_option(Some("subject"))
372 .unwrap()
373 .is_some());
374 assert!(EventSubject::from_option::<&str>(None).unwrap().is_none());
375 }
376}