use std::fmt::Display;
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MessageAge {
Days(i64),
Weeks(i64),
Months(i64),
Years(i64),
}
impl Display for MessageAge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageAge::Days(v) => write!(f, "d:{v}"),
MessageAge::Weeks(v) => write!(f, "w:{v}"),
MessageAge::Months(v) => write!(f, "m:{v}"),
MessageAge::Years(v) => write!(f, "y:{v}"),
}
}
}
impl MessageAge {
pub fn new(period: &str, count: i64) -> Result<Self> {
if count <= 0 {
return Err(Error::InvalidMessageAge(format!(
"Count must be positive, got: {count}"
)));
}
match period.to_lowercase().as_str() {
"days" => Ok(MessageAge::Days(count)),
"weeks" => Ok(MessageAge::Weeks(count)),
"months" => Ok(MessageAge::Months(count)),
"years" => Ok(MessageAge::Years(count)),
_ => Err(Error::InvalidMessageAge(format!(
"Unknown period '{period}', expected one of: days, weeks, months, years"
))),
}
}
#[must_use]
pub fn parse(s: &str) -> Option<MessageAge> {
let bytes = s.as_bytes();
if bytes.len() < 3 || bytes[1] != b':' {
return None;
}
let period = bytes[0];
let count_str = &s[2..];
let count = count_str.parse::<i64>().ok()?;
if count <= 0 {
return None;
}
match period {
b'd' => Some(MessageAge::Days(count)),
b'w' => Some(MessageAge::Weeks(count)),
b'm' => Some(MessageAge::Months(count)),
b'y' => Some(MessageAge::Years(count)),
_ => None,
}
}
#[must_use]
pub fn label(&self) -> String {
match self {
MessageAge::Days(v) => format!("retention/{v}-days"),
MessageAge::Weeks(v) => format!("retention/{v}-weeks"),
MessageAge::Months(v) => format!("retention/{v}-months"),
MessageAge::Years(v) => format!("retention/{v}-years"),
}
}
#[must_use]
pub fn value(&self) -> i64 {
match self {
MessageAge::Days(v)
| MessageAge::Weeks(v)
| MessageAge::Months(v)
| MessageAge::Years(v) => *v,
}
}
#[must_use]
pub fn period_type(&self) -> &'static str {
match self {
MessageAge::Days(_) => "days",
MessageAge::Weeks(_) => "weeks",
MessageAge::Months(_) => "months",
MessageAge::Years(_) => "years",
}
}
}
impl TryFrom<&str> for MessageAge {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
Self::parse(value).ok_or_else(|| {
Error::InvalidMessageAge(format!("Failed to parse MessageAge from '{value}'"))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_age_new_valid() {
assert_eq!(MessageAge::new("days", 30).unwrap(), MessageAge::Days(30));
assert_eq!(MessageAge::new("weeks", 4).unwrap(), MessageAge::Weeks(4));
assert_eq!(MessageAge::new("months", 6).unwrap(), MessageAge::Months(6));
assert_eq!(MessageAge::new("years", 2).unwrap(), MessageAge::Years(2));
assert_eq!(MessageAge::new("DAYS", 1).unwrap(), MessageAge::Days(1));
assert_eq!(MessageAge::new("Days", 1).unwrap(), MessageAge::Days(1));
assert_eq!(MessageAge::new("dAyS", 1).unwrap(), MessageAge::Days(1));
}
#[test]
fn test_message_age_new_invalid_period() {
assert!(MessageAge::new("invalid", 1).is_err());
assert!(MessageAge::new("day", 1).is_err()); assert!(MessageAge::new("", 1).is_err());
let err = MessageAge::new("invalid", 1).unwrap_err();
assert!(err.to_string().contains("Unknown period 'invalid'"));
}
#[test]
fn test_message_age_new_invalid_count() {
assert!(MessageAge::new("days", 0).is_err());
assert!(MessageAge::new("days", -1).is_err());
assert!(MessageAge::new("days", -100).is_err());
let err = MessageAge::new("days", -1).unwrap_err();
assert!(err.to_string().contains("Count must be positive"));
}
#[test]
fn test_message_age_parse_valid() {
assert_eq!(MessageAge::parse("d:30").unwrap(), MessageAge::Days(30));
assert_eq!(MessageAge::parse("w:4").unwrap(), MessageAge::Weeks(4));
assert_eq!(MessageAge::parse("m:6").unwrap(), MessageAge::Months(6));
assert_eq!(MessageAge::parse("y:2").unwrap(), MessageAge::Years(2));
assert_eq!(MessageAge::parse("d:999").unwrap(), MessageAge::Days(999));
}
#[test]
fn test_message_age_parse_invalid() {
assert!(MessageAge::parse("invalid").is_none());
assert!(MessageAge::parse("d").is_none());
assert!(MessageAge::parse("d:").is_none());
assert!(MessageAge::parse(":30").is_none());
assert!(MessageAge::parse("x:30").is_none());
assert!(MessageAge::parse("d:0").is_none());
assert!(MessageAge::parse("d:-1").is_none());
assert!(MessageAge::parse("d:abc").is_none());
assert!(MessageAge::parse("d-30").is_none());
assert!(MessageAge::parse("d 30").is_none());
}
#[test]
fn test_message_age_display() {
assert_eq!(MessageAge::Days(30).to_string(), "d:30");
assert_eq!(MessageAge::Weeks(4).to_string(), "w:4");
assert_eq!(MessageAge::Months(6).to_string(), "m:6");
assert_eq!(MessageAge::Years(2).to_string(), "y:2");
}
#[test]
fn test_message_age_label() {
assert_eq!(MessageAge::Days(30).label(), "retention/30-days");
assert_eq!(MessageAge::Weeks(4).label(), "retention/4-weeks");
assert_eq!(MessageAge::Months(6).label(), "retention/6-months");
assert_eq!(MessageAge::Years(2).label(), "retention/2-years");
}
#[test]
fn test_message_age_value() {
assert_eq!(MessageAge::Days(30).value(), 30);
assert_eq!(MessageAge::Weeks(4).value(), 4);
assert_eq!(MessageAge::Months(6).value(), 6);
assert_eq!(MessageAge::Years(2).value(), 2);
}
#[test]
fn test_message_age_period_type() {
assert_eq!(MessageAge::Days(30).period_type(), "days");
assert_eq!(MessageAge::Weeks(4).period_type(), "weeks");
assert_eq!(MessageAge::Months(6).period_type(), "months");
assert_eq!(MessageAge::Years(2).period_type(), "years");
}
#[test]
fn test_message_age_clone() {
let original = MessageAge::Days(30);
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_message_age_eq() {
assert_eq!(MessageAge::Days(30), MessageAge::Days(30));
assert_ne!(MessageAge::Days(30), MessageAge::Days(31));
assert_ne!(MessageAge::Days(30), MessageAge::Weeks(30));
}
#[test]
fn test_parse_roundtrip() {
let original = MessageAge::Days(30);
let serialized = original.to_string();
let parsed = MessageAge::parse(&serialized).unwrap();
assert_eq!(original, parsed);
let original = MessageAge::Years(5);
let serialized = original.to_string();
let parsed = MessageAge::parse(&serialized).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_try_from() {
use std::convert::TryFrom;
assert_eq!(MessageAge::try_from("d:30").unwrap(), MessageAge::Days(30));
assert_eq!(MessageAge::try_from("w:4").unwrap(), MessageAge::Weeks(4));
assert_eq!(MessageAge::try_from("m:6").unwrap(), MessageAge::Months(6));
assert_eq!(MessageAge::try_from("y:2").unwrap(), MessageAge::Years(2));
assert!(MessageAge::try_from("invalid").is_err());
assert!(MessageAge::try_from("d:-1").is_err());
}
}