✨ feat(retention): enhance message age with parsing and validation
- add `TryFrom<&str>` implementation for `MessageAge` - allows creating `MessageAge` from string slices - returns `Error` for invalid formats - improve `MessageAge::new` to return a `Result` - changes error type to `Error` enum - add `Error::InvalidMessageAge` for specific message age errors - mark `MessageAge::parse` and other methods as `must_use` - use byte string literals for character matching in `parse` - update error messages to include the invalid input value - add `#[derive(Hash)]` to `MessageAge`
This commit is contained in:
committed by
Jeremiah Russell
parent
5c2124ead4
commit
051507856a
@@ -1,28 +1,30 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use crate::{Error, Result};
|
||||||
|
|
||||||
/// Message age specification for retention policies.
|
/// Message age specification for retention policies.
|
||||||
///
|
///
|
||||||
/// Defines different time periods that can be used to specify how old messages
|
/// Defines different time periods that can be used to specify how old messages
|
||||||
/// should be before they are subject to retention actions (trash/delete).
|
/// should be before they are subject to retention actions (trash/delete).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
///
|
///
|
||||||
/// // Create different message age specifications
|
/// // Create different message age specifications
|
||||||
/// let days = MessageAge::Days(30);
|
/// let days = MessageAge::Days(30);
|
||||||
/// let weeks = MessageAge::Weeks(4);
|
/// let weeks = MessageAge::Weeks(4);
|
||||||
/// let months = MessageAge::Months(6);
|
/// let months = MessageAge::Months(6);
|
||||||
/// let years = MessageAge::Years(2);
|
/// let years = MessageAge::Years(2);
|
||||||
///
|
///
|
||||||
/// // Use with retention policy
|
/// // Use with retention policy
|
||||||
/// println!("Messages older than {} will be processed", months);
|
/// println!("Messages older than {} will be processed", months);
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum MessageAge {
|
pub enum MessageAge {
|
||||||
/// Number of days to retain the message
|
/// Number of days to retain the message
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
@@ -31,7 +33,7 @@ pub enum MessageAge {
|
|||||||
/// ```
|
/// ```
|
||||||
Days(i64),
|
Days(i64),
|
||||||
/// Number of weeks to retain the message
|
/// Number of weeks to retain the message
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
@@ -40,7 +42,7 @@ pub enum MessageAge {
|
|||||||
/// ```
|
/// ```
|
||||||
Weeks(i64),
|
Weeks(i64),
|
||||||
/// Number of months to retain the message
|
/// Number of months to retain the message
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
@@ -49,7 +51,7 @@ pub enum MessageAge {
|
|||||||
/// ```
|
/// ```
|
||||||
Months(i64),
|
Months(i64),
|
||||||
/// Number of years to retain the message
|
/// Number of years to retain the message
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
@@ -71,109 +73,116 @@ impl Display for MessageAge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageAge {
|
impl MessageAge {
|
||||||
/// Create a new MessageAge from a period string and count.
|
/// Create a new `MessageAge` from a period string and count.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `period` - The time period ("days", "weeks", "months", "years")
|
/// * `period` - The time period ("days", "weeks", "months", "years")
|
||||||
/// * `count` - The number of time periods (must be positive)
|
/// * `count` - The number of time periods (must be positive)
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::new("days", 30).unwrap();
|
/// let age = MessageAge::new("days", 30).unwrap();
|
||||||
/// assert_eq!(age, MessageAge::Days(30));
|
/// assert_eq!(age, MessageAge::Days(30));
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::new("months", 6).unwrap();
|
/// let age = MessageAge::new("months", 6).unwrap();
|
||||||
/// assert_eq!(age, MessageAge::Months(6));
|
/// assert_eq!(age, MessageAge::Months(6));
|
||||||
///
|
///
|
||||||
/// // Invalid period returns an error
|
/// // Invalid period returns an error
|
||||||
/// assert!(MessageAge::new("invalid", 1).is_err());
|
/// assert!(MessageAge::new("invalid", 1).is_err());
|
||||||
///
|
///
|
||||||
/// // Negative count returns an error
|
/// // Negative count returns an error
|
||||||
/// assert!(MessageAge::new("days", -1).is_err());
|
/// assert!(MessageAge::new("days", -1).is_err());
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if:
|
/// Returns an error if:
|
||||||
/// - The period string is not recognized
|
/// - The period string is not recognized
|
||||||
/// - The count is negative or zero
|
/// - The count is negative or zero
|
||||||
pub fn new(period: &str, count: i64) -> Result<Self, String> {
|
pub fn new(period: &str, count: i64) -> Result<Self> {
|
||||||
if count <= 0 {
|
if count <= 0 {
|
||||||
return Err(format!("Count must be positive, got: {}", count));
|
return Err(Error::InvalidMessageAge(format!(
|
||||||
|
"Count must be positive, got: {count}"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
match period.to_lowercase().as_str() {
|
match period.to_lowercase().as_str() {
|
||||||
"days" => Ok(MessageAge::Days(count)),
|
"days" => Ok(MessageAge::Days(count)),
|
||||||
"weeks" => Ok(MessageAge::Weeks(count)),
|
"weeks" => Ok(MessageAge::Weeks(count)),
|
||||||
"months" => Ok(MessageAge::Months(count)),
|
"months" => Ok(MessageAge::Months(count)),
|
||||||
"years" => Ok(MessageAge::Years(count)),
|
"years" => Ok(MessageAge::Years(count)),
|
||||||
_ => Err(format!("Unknown period '{}', expected one of: days, weeks, months, years", period)),
|
_ => Err(Error::InvalidMessageAge(format!(
|
||||||
|
"Unknown period '{period}', expected one of: days, weeks, months, years"
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a MessageAge from a string representation (e.g., "d:30", "m:6").
|
/// Parse a `MessageAge` from a string representation (e.g., "d:30", "m:6").
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `s` - String in format "`period:count`" where period is d/w/m/y
|
/// * `s` - String in format "`period:count`" where period is d/w/m/y
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::parse("d:30").unwrap();
|
/// let age = MessageAge::parse("d:30").unwrap();
|
||||||
/// assert_eq!(age, MessageAge::Days(30));
|
/// assert_eq!(age, MessageAge::Days(30));
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::parse("y:2").unwrap();
|
/// let age = MessageAge::parse("y:2").unwrap();
|
||||||
/// assert_eq!(age, MessageAge::Years(2));
|
/// assert_eq!(age, MessageAge::Years(2));
|
||||||
///
|
///
|
||||||
/// // Invalid format returns None
|
/// // Invalid format returns None
|
||||||
/// assert!(MessageAge::parse("invalid").is_none());
|
/// assert!(MessageAge::parse("invalid").is_none());
|
||||||
/// assert!(MessageAge::parse("d").is_none());
|
/// assert!(MessageAge::parse("d").is_none());
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub fn parse(s: &str) -> Option<MessageAge> {
|
pub fn parse(s: &str) -> Option<MessageAge> {
|
||||||
if s.len() < 3 || s.chars().nth(1) != Some(':') {
|
let bytes = s.as_bytes();
|
||||||
|
if bytes.len() < 3 || bytes[1] != b':' {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let period = s.chars().nth(0)?;
|
let period = bytes[0];
|
||||||
let count_str = &s[2..];
|
let count_str = &s[2..];
|
||||||
let count = count_str.parse::<i64>().ok()?;
|
let count = count_str.parse::<i64>().ok()?;
|
||||||
|
|
||||||
if count <= 0 {
|
if count <= 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
match period {
|
match period {
|
||||||
'd' => Some(MessageAge::Days(count)),
|
b'd' => Some(MessageAge::Days(count)),
|
||||||
'w' => Some(MessageAge::Weeks(count)),
|
b'w' => Some(MessageAge::Weeks(count)),
|
||||||
'm' => Some(MessageAge::Months(count)),
|
b'm' => Some(MessageAge::Months(count)),
|
||||||
'y' => Some(MessageAge::Years(count)),
|
b'y' => Some(MessageAge::Years(count)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a label string for this message age.
|
/// Generate a label string for this message age.
|
||||||
///
|
///
|
||||||
/// This creates a standardized label that can be used to categorize
|
/// This creates a standardized label that can be used to categorize
|
||||||
/// messages based on their retention period.
|
/// messages based on their retention period.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::Days(30);
|
/// let age = MessageAge::Days(30);
|
||||||
/// assert_eq!(age.label(), "retention/30-days");
|
/// assert_eq!(age.label(), "retention/30-days");
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::Years(1);
|
/// let age = MessageAge::Years(1);
|
||||||
/// assert_eq!(age.label(), "retention/1-years");
|
/// assert_eq!(age.label(), "retention/1-years");
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub fn label(&self) -> String {
|
pub fn label(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
MessageAge::Days(v) => format!("retention/{v}-days"),
|
MessageAge::Days(v) => format!("retention/{v}-days"),
|
||||||
@@ -182,39 +191,44 @@ impl MessageAge {
|
|||||||
MessageAge::Years(v) => format!("retention/{v}-years"),
|
MessageAge::Years(v) => format!("retention/{v}-years"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the numeric value of this message age.
|
/// Get the numeric value of this message age.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::Days(30);
|
/// let age = MessageAge::Days(30);
|
||||||
/// assert_eq!(age.value(), 30);
|
/// assert_eq!(age.value(), 30);
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::Years(2);
|
/// let age = MessageAge::Years(2);
|
||||||
/// assert_eq!(age.value(), 2);
|
/// assert_eq!(age.value(), 2);
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub fn value(&self) -> i64 {
|
pub fn value(&self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
MessageAge::Days(v) | MessageAge::Weeks(v) | MessageAge::Months(v) | MessageAge::Years(v) => *v,
|
MessageAge::Days(v)
|
||||||
|
| MessageAge::Weeks(v)
|
||||||
|
| MessageAge::Months(v)
|
||||||
|
| MessageAge::Years(v) => *v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the period type as a string.
|
/// Get the period type as a string.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use cull_gmail::MessageAge;
|
/// use cull_gmail::MessageAge;
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::Days(30);
|
/// let age = MessageAge::Days(30);
|
||||||
/// assert_eq!(age.period_type(), "days");
|
/// assert_eq!(age.period_type(), "days");
|
||||||
///
|
///
|
||||||
/// let age = MessageAge::Years(2);
|
/// let age = MessageAge::Years(2);
|
||||||
/// assert_eq!(age.period_type(), "years");
|
/// assert_eq!(age.period_type(), "years");
|
||||||
/// ```
|
/// ```
|
||||||
|
#[must_use]
|
||||||
pub fn period_type(&self) -> &'static str {
|
pub fn period_type(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
MessageAge::Days(_) => "days",
|
MessageAge::Days(_) => "days",
|
||||||
@@ -225,6 +239,30 @@ impl MessageAge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for MessageAge {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
/// Try to create a `MessageAge` from a string using the parse format.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use cull_gmail::MessageAge;
|
||||||
|
/// use std::convert::TryFrom;
|
||||||
|
///
|
||||||
|
/// let age = MessageAge::try_from("d:30").unwrap();
|
||||||
|
/// assert_eq!(age, MessageAge::Days(30));
|
||||||
|
///
|
||||||
|
/// let age = MessageAge::try_from("invalid");
|
||||||
|
/// assert!(age.is_err());
|
||||||
|
/// ```
|
||||||
|
fn try_from(value: &str) -> Result<Self> {
|
||||||
|
Self::parse(value).ok_or_else(|| {
|
||||||
|
Error::InvalidMessageAge(format!("Failed to parse MessageAge from '{value}'"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -236,7 +274,7 @@ mod tests {
|
|||||||
assert_eq!(MessageAge::new("weeks", 4).unwrap(), MessageAge::Weeks(4));
|
assert_eq!(MessageAge::new("weeks", 4).unwrap(), MessageAge::Weeks(4));
|
||||||
assert_eq!(MessageAge::new("months", 6).unwrap(), MessageAge::Months(6));
|
assert_eq!(MessageAge::new("months", 6).unwrap(), MessageAge::Months(6));
|
||||||
assert_eq!(MessageAge::new("years", 2).unwrap(), MessageAge::Years(2));
|
assert_eq!(MessageAge::new("years", 2).unwrap(), MessageAge::Years(2));
|
||||||
|
|
||||||
// Test case insensitive
|
// Test case insensitive
|
||||||
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));
|
assert_eq!(MessageAge::new("Days", 1).unwrap(), MessageAge::Days(1));
|
||||||
@@ -246,12 +284,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_message_age_new_invalid_period() {
|
fn test_message_age_new_invalid_period() {
|
||||||
assert!(MessageAge::new("invalid", 1).is_err());
|
assert!(MessageAge::new("invalid", 1).is_err());
|
||||||
assert!(MessageAge::new("day", 1).is_err()); // singular form
|
assert!(MessageAge::new("day", 1).is_err()); // singular form
|
||||||
assert!(MessageAge::new("", 1).is_err());
|
assert!(MessageAge::new("", 1).is_err());
|
||||||
|
|
||||||
// Check error messages
|
// Check error messages
|
||||||
let err = MessageAge::new("invalid", 1).unwrap_err();
|
let err = MessageAge::new("invalid", 1).unwrap_err();
|
||||||
assert!(err.contains("Unknown period 'invalid'"));
|
assert!(err.to_string().contains("Unknown period 'invalid'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -259,10 +297,10 @@ mod tests {
|
|||||||
assert!(MessageAge::new("days", 0).is_err());
|
assert!(MessageAge::new("days", 0).is_err());
|
||||||
assert!(MessageAge::new("days", -1).is_err());
|
assert!(MessageAge::new("days", -1).is_err());
|
||||||
assert!(MessageAge::new("days", -100).is_err());
|
assert!(MessageAge::new("days", -100).is_err());
|
||||||
|
|
||||||
// Check error messages
|
// Check error messages
|
||||||
let err = MessageAge::new("days", -1).unwrap_err();
|
let err = MessageAge::new("days", -1).unwrap_err();
|
||||||
assert!(err.contains("Count must be positive"));
|
assert!(err.to_string().contains("Count must be positive"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -271,7 +309,7 @@ mod tests {
|
|||||||
assert_eq!(MessageAge::parse("w:4").unwrap(), MessageAge::Weeks(4));
|
assert_eq!(MessageAge::parse("w:4").unwrap(), MessageAge::Weeks(4));
|
||||||
assert_eq!(MessageAge::parse("m:6").unwrap(), MessageAge::Months(6));
|
assert_eq!(MessageAge::parse("m:6").unwrap(), MessageAge::Months(6));
|
||||||
assert_eq!(MessageAge::parse("y:2").unwrap(), MessageAge::Years(2));
|
assert_eq!(MessageAge::parse("y:2").unwrap(), MessageAge::Years(2));
|
||||||
|
|
||||||
// Test large numbers
|
// Test large numbers
|
||||||
assert_eq!(MessageAge::parse("d:999").unwrap(), MessageAge::Days(999));
|
assert_eq!(MessageAge::parse("d:999").unwrap(), MessageAge::Days(999));
|
||||||
}
|
}
|
||||||
@@ -284,12 +322,12 @@ mod tests {
|
|||||||
assert!(MessageAge::parse("d:").is_none());
|
assert!(MessageAge::parse("d:").is_none());
|
||||||
assert!(MessageAge::parse(":30").is_none());
|
assert!(MessageAge::parse(":30").is_none());
|
||||||
assert!(MessageAge::parse("x:30").is_none());
|
assert!(MessageAge::parse("x:30").is_none());
|
||||||
|
|
||||||
// Invalid count
|
// Invalid count
|
||||||
assert!(MessageAge::parse("d:0").is_none());
|
assert!(MessageAge::parse("d:0").is_none());
|
||||||
assert!(MessageAge::parse("d:-1").is_none());
|
assert!(MessageAge::parse("d:-1").is_none());
|
||||||
assert!(MessageAge::parse("d:abc").is_none());
|
assert!(MessageAge::parse("d:abc").is_none());
|
||||||
|
|
||||||
// Wrong separator
|
// Wrong separator
|
||||||
assert!(MessageAge::parse("d-30").is_none());
|
assert!(MessageAge::parse("d-30").is_none());
|
||||||
assert!(MessageAge::parse("d 30").is_none());
|
assert!(MessageAge::parse("d 30").is_none());
|
||||||
@@ -347,10 +385,23 @@ mod tests {
|
|||||||
let serialized = original.to_string();
|
let serialized = original.to_string();
|
||||||
let parsed = MessageAge::parse(&serialized).unwrap();
|
let parsed = MessageAge::parse(&serialized).unwrap();
|
||||||
assert_eq!(original, parsed);
|
assert_eq!(original, parsed);
|
||||||
|
|
||||||
let original = MessageAge::Years(5);
|
let original = MessageAge::Years(5);
|
||||||
let serialized = original.to_string();
|
let serialized = original.to_string();
|
||||||
let parsed = MessageAge::parse(&serialized).unwrap();
|
let parsed = MessageAge::parse(&serialized).unwrap();
|
||||||
assert_eq!(original, parsed);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user