cull_gmail/retention/
message_age.rs

1use std::fmt::Display;
2
3use crate::{Error, Result};
4
5/// Message age specification for retention policies.
6///
7/// Defines different time periods that can be used to specify how old messages
8/// should be before they are subject to retention actions (trash/delete).
9///
10/// # Examples
11///
12/// ```
13/// use cull_gmail::MessageAge;
14///
15/// // Create different message age specifications
16/// let days = MessageAge::Days(30);
17/// let weeks = MessageAge::Weeks(4);
18/// let months = MessageAge::Months(6);
19/// let years = MessageAge::Years(2);
20///
21/// // Use with retention policy
22/// println!("Messages older than {} will be processed", months);
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub enum MessageAge {
26    /// Number of days to retain the message
27    ///
28    /// # Example
29    /// ```
30    /// use cull_gmail::MessageAge;
31    /// let age = MessageAge::Days(30);
32    /// assert_eq!(age.to_string(), "d:30");
33    /// ```
34    Days(i64),
35    /// Number of weeks to retain the message
36    ///
37    /// # Example
38    /// ```
39    /// use cull_gmail::MessageAge;
40    /// let age = MessageAge::Weeks(4);
41    /// assert_eq!(age.to_string(), "w:4");
42    /// ```
43    Weeks(i64),
44    /// Number of months to retain the message
45    ///
46    /// # Example
47    /// ```
48    /// use cull_gmail::MessageAge;
49    /// let age = MessageAge::Months(6);
50    /// assert_eq!(age.to_string(), "m:6");
51    /// ```
52    Months(i64),
53    /// Number of years to retain the message
54    ///
55    /// # Example
56    /// ```
57    /// use cull_gmail::MessageAge;
58    /// let age = MessageAge::Years(2);
59    /// assert_eq!(age.to_string(), "y:2");
60    /// ```
61    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    /// Create a new `MessageAge` from a period string and count.
77    ///
78    /// # Arguments
79    ///
80    /// * `period` - The time period ("days", "weeks", "months", "years")
81    /// * `count` - The number of time periods (must be positive)
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use cull_gmail::MessageAge;
87    ///
88    /// let age = MessageAge::new("days", 30).unwrap();
89    /// assert_eq!(age, MessageAge::Days(30));
90    ///
91    /// let age = MessageAge::new("months", 6).unwrap();
92    /// assert_eq!(age, MessageAge::Months(6));
93    ///
94    /// // Invalid period returns an error
95    /// assert!(MessageAge::new("invalid", 1).is_err());
96    ///
97    /// // Negative count returns an error
98    /// assert!(MessageAge::new("days", -1).is_err());
99    /// ```
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if:
104    /// - The period string is not recognized
105    /// - The count is negative or zero
106    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    /// Parse a `MessageAge` from a string representation (e.g., "d:30", "m:6").
125    ///
126    /// # Arguments
127    ///
128    /// * `s` - String in format "`period:count`" where period is d/w/m/y
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use cull_gmail::MessageAge;
134    ///
135    /// let age = MessageAge::parse("d:30").unwrap();
136    /// assert_eq!(age, MessageAge::Days(30));
137    ///
138    /// let age = MessageAge::parse("y:2").unwrap();
139    /// assert_eq!(age, MessageAge::Years(2));
140    ///
141    /// // Invalid format returns None
142    /// assert!(MessageAge::parse("invalid").is_none());
143    /// assert!(MessageAge::parse("d").is_none());
144    /// ```
145    #[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    /// Generate a label string for this message age.
170    ///
171    /// This creates a standardized label that can be used to categorize
172    /// messages based on their retention period.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use cull_gmail::MessageAge;
178    ///
179    /// let age = MessageAge::Days(30);
180    /// assert_eq!(age.label(), "retention/30-days");
181    ///
182    /// let age = MessageAge::Years(1);
183    /// assert_eq!(age.label(), "retention/1-years");
184    /// ```
185    #[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    /// Get the numeric value of this message age.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use cull_gmail::MessageAge;
201    ///
202    /// let age = MessageAge::Days(30);
203    /// assert_eq!(age.value(), 30);
204    ///
205    /// let age = MessageAge::Years(2);
206    /// assert_eq!(age.value(), 2);
207    /// ```
208    #[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    /// Get the period type as a string.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use cull_gmail::MessageAge;
224    ///
225    /// let age = MessageAge::Days(30);
226    /// assert_eq!(age.period_type(), "days");
227    ///
228    /// let age = MessageAge::Years(2);
229    /// assert_eq!(age.period_type(), "years");
230    /// ```
231    #[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    /// Try to create a `MessageAge` from a string using the parse format.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use cull_gmail::MessageAge;
251    /// use std::convert::TryFrom;
252    ///
253    /// let age = MessageAge::try_from("d:30").unwrap();
254    /// assert_eq!(age, MessageAge::Days(30));
255    ///
256    /// let age = MessageAge::try_from("invalid");
257    /// assert!(age.is_err());
258    /// ```
259    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        // Test valid periods
273        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        // Test case insensitive
279        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()); // singular form
288        assert!(MessageAge::new("", 1).is_err());
289
290        // Check error messages
291        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        // Check error messages
302        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        // Test large numbers
314        assert_eq!(MessageAge::parse("d:999").unwrap(), MessageAge::Days(999));
315    }
316
317    #[test]
318    fn test_message_age_parse_invalid() {
319        // Invalid format
320        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        // Invalid count
327        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        // Wrong separator
332        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}