1use std::fmt::Display;
2
3use crate::{Error, Result};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub enum MessageAge {
26 Days(i64),
35 Weeks(i64),
44 Months(i64),
53 Years(i64),
62}
63
64impl Display for MessageAge {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 MessageAge::Days(v) => write!(f, "d:{v}"),
68 MessageAge::Weeks(v) => write!(f, "w:{v}"),
69 MessageAge::Months(v) => write!(f, "m:{v}"),
70 MessageAge::Years(v) => write!(f, "y:{v}"),
71 }
72 }
73}
74
75impl MessageAge {
76 pub fn new(period: &str, count: i64) -> Result<Self> {
107 if count <= 0 {
108 return Err(Error::InvalidMessageAge(format!(
109 "Count must be positive, got: {count}"
110 )));
111 }
112
113 match period.to_lowercase().as_str() {
114 "days" => Ok(MessageAge::Days(count)),
115 "weeks" => Ok(MessageAge::Weeks(count)),
116 "months" => Ok(MessageAge::Months(count)),
117 "years" => Ok(MessageAge::Years(count)),
118 _ => Err(Error::InvalidMessageAge(format!(
119 "Unknown period '{period}', expected one of: days, weeks, months, years"
120 ))),
121 }
122 }
123
124 #[must_use]
146 pub fn parse(s: &str) -> Option<MessageAge> {
147 let bytes = s.as_bytes();
148 if bytes.len() < 3 || bytes[1] != b':' {
149 return None;
150 }
151
152 let period = bytes[0];
153 let count_str = &s[2..];
154 let count = count_str.parse::<i64>().ok()?;
155
156 if count <= 0 {
157 return None;
158 }
159
160 match period {
161 b'd' => Some(MessageAge::Days(count)),
162 b'w' => Some(MessageAge::Weeks(count)),
163 b'm' => Some(MessageAge::Months(count)),
164 b'y' => Some(MessageAge::Years(count)),
165 _ => None,
166 }
167 }
168
169 #[must_use]
186 pub fn label(&self) -> String {
187 match self {
188 MessageAge::Days(v) => format!("retention/{v}-days"),
189 MessageAge::Weeks(v) => format!("retention/{v}-weeks"),
190 MessageAge::Months(v) => format!("retention/{v}-months"),
191 MessageAge::Years(v) => format!("retention/{v}-years"),
192 }
193 }
194
195 #[must_use]
209 pub fn value(&self) -> i64 {
210 match self {
211 MessageAge::Days(v)
212 | MessageAge::Weeks(v)
213 | MessageAge::Months(v)
214 | MessageAge::Years(v) => *v,
215 }
216 }
217
218 #[must_use]
232 pub fn period_type(&self) -> &'static str {
233 match self {
234 MessageAge::Days(_) => "days",
235 MessageAge::Weeks(_) => "weeks",
236 MessageAge::Months(_) => "months",
237 MessageAge::Years(_) => "years",
238 }
239 }
240}
241
242impl TryFrom<&str> for MessageAge {
243 type Error = Error;
244
245 fn try_from(value: &str) -> Result<Self> {
260 Self::parse(value).ok_or_else(|| {
261 Error::InvalidMessageAge(format!("Failed to parse MessageAge from '{value}'"))
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_message_age_new_valid() {
272 assert_eq!(MessageAge::new("days", 30).unwrap(), MessageAge::Days(30));
274 assert_eq!(MessageAge::new("weeks", 4).unwrap(), MessageAge::Weeks(4));
275 assert_eq!(MessageAge::new("months", 6).unwrap(), MessageAge::Months(6));
276 assert_eq!(MessageAge::new("years", 2).unwrap(), MessageAge::Years(2));
277
278 assert_eq!(MessageAge::new("DAYS", 1).unwrap(), MessageAge::Days(1));
280 assert_eq!(MessageAge::new("Days", 1).unwrap(), MessageAge::Days(1));
281 assert_eq!(MessageAge::new("dAyS", 1).unwrap(), MessageAge::Days(1));
282 }
283
284 #[test]
285 fn test_message_age_new_invalid_period() {
286 assert!(MessageAge::new("invalid", 1).is_err());
287 assert!(MessageAge::new("day", 1).is_err()); assert!(MessageAge::new("", 1).is_err());
289
290 let err = MessageAge::new("invalid", 1).unwrap_err();
292 assert!(err.to_string().contains("Unknown period 'invalid'"));
293 }
294
295 #[test]
296 fn test_message_age_new_invalid_count() {
297 assert!(MessageAge::new("days", 0).is_err());
298 assert!(MessageAge::new("days", -1).is_err());
299 assert!(MessageAge::new("days", -100).is_err());
300
301 let err = MessageAge::new("days", -1).unwrap_err();
303 assert!(err.to_string().contains("Count must be positive"));
304 }
305
306 #[test]
307 fn test_message_age_parse_valid() {
308 assert_eq!(MessageAge::parse("d:30").unwrap(), MessageAge::Days(30));
309 assert_eq!(MessageAge::parse("w:4").unwrap(), MessageAge::Weeks(4));
310 assert_eq!(MessageAge::parse("m:6").unwrap(), MessageAge::Months(6));
311 assert_eq!(MessageAge::parse("y:2").unwrap(), MessageAge::Years(2));
312
313 assert_eq!(MessageAge::parse("d:999").unwrap(), MessageAge::Days(999));
315 }
316
317 #[test]
318 fn test_message_age_parse_invalid() {
319 assert!(MessageAge::parse("invalid").is_none());
321 assert!(MessageAge::parse("d").is_none());
322 assert!(MessageAge::parse("d:").is_none());
323 assert!(MessageAge::parse(":30").is_none());
324 assert!(MessageAge::parse("x:30").is_none());
325
326 assert!(MessageAge::parse("d:0").is_none());
328 assert!(MessageAge::parse("d:-1").is_none());
329 assert!(MessageAge::parse("d:abc").is_none());
330
331 assert!(MessageAge::parse("d-30").is_none());
333 assert!(MessageAge::parse("d 30").is_none());
334 }
335
336 #[test]
337 fn test_message_age_display() {
338 assert_eq!(MessageAge::Days(30).to_string(), "d:30");
339 assert_eq!(MessageAge::Weeks(4).to_string(), "w:4");
340 assert_eq!(MessageAge::Months(6).to_string(), "m:6");
341 assert_eq!(MessageAge::Years(2).to_string(), "y:2");
342 }
343
344 #[test]
345 fn test_message_age_label() {
346 assert_eq!(MessageAge::Days(30).label(), "retention/30-days");
347 assert_eq!(MessageAge::Weeks(4).label(), "retention/4-weeks");
348 assert_eq!(MessageAge::Months(6).label(), "retention/6-months");
349 assert_eq!(MessageAge::Years(2).label(), "retention/2-years");
350 }
351
352 #[test]
353 fn test_message_age_value() {
354 assert_eq!(MessageAge::Days(30).value(), 30);
355 assert_eq!(MessageAge::Weeks(4).value(), 4);
356 assert_eq!(MessageAge::Months(6).value(), 6);
357 assert_eq!(MessageAge::Years(2).value(), 2);
358 }
359
360 #[test]
361 fn test_message_age_period_type() {
362 assert_eq!(MessageAge::Days(30).period_type(), "days");
363 assert_eq!(MessageAge::Weeks(4).period_type(), "weeks");
364 assert_eq!(MessageAge::Months(6).period_type(), "months");
365 assert_eq!(MessageAge::Years(2).period_type(), "years");
366 }
367
368 #[test]
369 fn test_message_age_clone() {
370 let original = MessageAge::Days(30);
371 let cloned = original.clone();
372 assert_eq!(original, cloned);
373 }
374
375 #[test]
376 fn test_message_age_eq() {
377 assert_eq!(MessageAge::Days(30), MessageAge::Days(30));
378 assert_ne!(MessageAge::Days(30), MessageAge::Days(31));
379 assert_ne!(MessageAge::Days(30), MessageAge::Weeks(30));
380 }
381
382 #[test]
383 fn test_parse_roundtrip() {
384 let original = MessageAge::Days(30);
385 let serialized = original.to_string();
386 let parsed = MessageAge::parse(&serialized).unwrap();
387 assert_eq!(original, parsed);
388
389 let original = MessageAge::Years(5);
390 let serialized = original.to_string();
391 let parsed = MessageAge::parse(&serialized).unwrap();
392 assert_eq!(original, parsed);
393 }
394
395 #[test]
396 fn test_try_from() {
397 use std::convert::TryFrom;
398
399 assert_eq!(MessageAge::try_from("d:30").unwrap(), MessageAge::Days(30));
400 assert_eq!(MessageAge::try_from("w:4").unwrap(), MessageAge::Weeks(4));
401 assert_eq!(MessageAge::try_from("m:6").unwrap(), MessageAge::Months(6));
402 assert_eq!(MessageAge::try_from("y:2").unwrap(), MessageAge::Years(2));
403
404 assert!(MessageAge::try_from("invalid").is_err());
405 assert!(MessageAge::try_from("d:-1").is_err());
406 }
407}