cull_gmail/
eol_action.rs

1//! # End-of-Life Action Module
2//!
3//! This module defines the actions that can be performed on Gmail messages
4//! when they reach their end-of-life criteria based on configured rules.
5//!
6//! ## Overview
7//!
8//! The `EolAction` enum specifies how messages should be handled when they
9//! meet the criteria for removal from a Gmail account. The module provides
10//! two primary actions:
11//!
12//! - **Trash**: Moves messages to the trash folder (reversible)
13//! - **Delete**: Permanently deletes messages (irreversible)
14//!
15//! ## Safety Considerations
16//!
17//! - **Trash** action allows message recovery from Gmail's trash folder
18//! - **Delete** action permanently removes messages and cannot be undone
19//! - Always test rules carefully before applying delete actions
20//!
21//! ## Usage Examples
22//!
23//! ### Basic Usage
24//!
25//! ```rust
26//! use cull_gmail::EolAction;
27//!
28//! // Default action is Trash (safer option)
29//! let action = EolAction::default();
30//! assert_eq!(action, EolAction::Trash);
31//!
32//! // Parse from string
33//! let delete_action = EolAction::parse("delete").unwrap();
34//! assert_eq!(delete_action, EolAction::Delete);
35//!
36//! // Display as string
37//! println!("Action: {}", delete_action); // Prints: "Action: delete"
38//! ```
39//!
40//! ### Integration with Rules
41//!
42//! ```rust,no_run
43//! use cull_gmail::EolAction;
44//!
45//! fn configure_rule_action(action_str: &str) -> Option<EolAction> {
46//!     match EolAction::parse(action_str) {
47//!         Some(action) => {
48//!             println!("Configured action: {}", action);
49//!             Some(action)
50//!         }
51//!         None => {
52//!             eprintln!("Invalid action: {}", action_str);
53//!             None
54//!         }
55//!     }
56//! }
57//! ```
58//!
59//! ## String Representation
60//!
61//! The enum implements both parsing from strings and display formatting:
62//!
63//! | Variant | String | Description |
64//! |---------|--------|--------------|
65//! | `Trash` | "trash" | Move to trash (recoverable) |
66//! | `Delete` | "delete" | Permanent deletion |
67//!
68//! Parsing is case-insensitive, so "TRASH", "Trash", and "trash" are all valid.
69
70use std::fmt;
71
72/// Represents the action to take on Gmail messages that meet end-of-life criteria.
73///
74/// This enum defines the two possible actions for handling messages when they
75/// reach the end of their lifecycle based on configured retention rules.
76///
77/// # Variants
78///
79/// - [`Trash`](EolAction::Trash) - Move messages to Gmail's trash folder (default, reversible)
80/// - [`Delete`](EolAction::Delete) - Permanently delete messages (irreversible)
81///
82/// # Default Behavior
83///
84/// The default action is [`Trash`](EolAction::Trash), which provides a safety net
85/// by allowing message recovery from the trash folder.
86///
87/// # Examples
88///
89/// ```rust
90/// use cull_gmail::EolAction;
91///
92/// // Using the default (Trash)
93/// let safe_action = EolAction::default();
94/// assert_eq!(safe_action, EolAction::Trash);
95///
96/// // Comparing actions
97/// let delete = EolAction::Delete;
98/// let trash = EolAction::Trash;
99/// assert_ne!(delete, trash);
100///
101/// // Converting to string for logging/display
102/// println!("Action: {}", delete); // Prints: "delete"
103/// ```
104#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
105pub enum EolAction {
106    /// Move the message to Gmail's trash folder.
107    ///
108    /// This is the default and safer option as it allows message recovery.
109    /// Messages in the trash are automatically deleted by Gmail after 30 days.
110    ///
111    /// # Safety
112    ///
113    /// This action is reversible - messages can be recovered from the trash folder
114    /// until they are automatically purged or manually deleted from trash.
115    #[default]
116    Trash,
117
118    /// Permanently delete the message immediately.
119    ///
120    /// This action bypasses the trash folder and permanently removes the message.
121    ///
122    /// # Warning
123    ///
124    /// This action is **irreversible**. Once deleted, messages cannot be recovered.
125    /// Use with extreme caution and thorough testing of rule criteria.
126    ///
127    /// # Use Cases
128    ///
129    /// - Sensitive data that should not remain in trash
130    /// - Storage optimization where trash recovery is not needed
131    /// - Automated cleanup of known disposable messages
132    Delete,
133}
134
135impl fmt::Display for EolAction {
136    /// Formats the `EolAction` as a lowercase string.
137    ///
138    /// This implementation provides a consistent string representation
139    /// for logging, configuration, and user interfaces.
140    ///
141    /// # Returns
142    ///
143    /// - `"trash"` for [`EolAction::Trash`]
144    /// - `"delete"` for [`EolAction::Delete`]
145    ///
146    /// # Examples
147    ///
148    /// ```rust
149    /// use cull_gmail::EolAction;
150    ///
151    /// assert_eq!(EolAction::Trash.to_string(), "trash");
152    /// assert_eq!(EolAction::Delete.to_string(), "delete");
153    ///
154    /// // Useful for logging
155    /// let action = EolAction::default();
156    /// println!("Performing action: {}", action);
157    /// ```
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            EolAction::Trash => write!(f, "trash"),
161            EolAction::Delete => write!(f, "delete"),
162        }
163    }
164}
165
166impl EolAction {
167    /// Parses a string into an `EolAction` variant.
168    ///
169    /// This method provides case-insensitive parsing from string representations
170    /// to `EolAction` variants. It's useful for configuration file parsing,
171    /// command-line arguments, and user input validation.
172    ///
173    /// # Arguments
174    ///
175    /// * `input` - A string slice to parse. Case is ignored.
176    ///
177    /// # Returns
178    ///
179    /// - `Some(EolAction)` if the string matches a valid variant
180    /// - `None` if the string is not recognized
181    ///
182    /// # Valid Input Strings
183    ///
184    /// - `"trash"`, `"Trash"`, `"TRASH"` → [`EolAction::Trash`]
185    /// - `"delete"`, `"Delete"`, `"DELETE"` → [`EolAction::Delete`]
186    ///
187    /// # Examples
188    ///
189    /// ```rust
190    /// use cull_gmail::EolAction;
191    ///
192    /// // Valid parsing (case-insensitive)
193    /// assert_eq!(EolAction::parse("trash"), Some(EolAction::Trash));
194    /// assert_eq!(EolAction::parse("TRASH"), Some(EolAction::Trash));
195    /// assert_eq!(EolAction::parse("Delete"), Some(EolAction::Delete));
196    ///
197    /// // Invalid input
198    /// assert_eq!(EolAction::parse("invalid"), None);
199    /// assert_eq!(EolAction::parse(""), None);
200    /// ```
201    ///
202    /// # Use Cases
203    ///
204    /// ```rust
205    /// use cull_gmail::EolAction;
206    ///
207    /// fn parse_user_action(input: &str) -> Result<EolAction, String> {
208    ///     EolAction::parse(input)
209    ///         .ok_or_else(|| format!("Invalid action: '{}'. Use 'trash' or 'delete'.", input))
210    /// }
211    ///
212    /// assert!(parse_user_action("trash").is_ok());
213    /// assert!(parse_user_action("invalid").is_err());
214    /// ```
215    pub fn parse(input: &str) -> Option<Self> {
216        match input.trim().to_lowercase().as_str() {
217            "trash" => Some(EolAction::Trash),
218            "delete" => Some(EolAction::Delete),
219            _ => None,
220        }
221    }
222
223    /// Returns `true` if the action is reversible (can be undone).
224    ///
225    /// This method helps determine if an action allows for message recovery,
226    /// which is useful for safety checks and user confirmations.
227    ///
228    /// # Returns
229    ///
230    /// - `true` for [`EolAction::Trash`] (messages can be recovered from trash)
231    /// - `false` for [`EolAction::Delete`] (messages are permanently deleted)
232    ///
233    /// # Examples
234    ///
235    /// ```rust
236    /// use cull_gmail::EolAction;
237    ///
238    /// assert!(EolAction::Trash.is_reversible());
239    /// assert!(!EolAction::Delete.is_reversible());
240    ///
241    /// // Use in safety checks
242    /// let action = EolAction::Delete;
243    /// if !action.is_reversible() {
244    ///     println!("Warning: This action cannot be undone!");
245    /// }
246    /// ```
247    pub fn is_reversible(&self) -> bool {
248        match self {
249            EolAction::Trash => true,
250            EolAction::Delete => false,
251        }
252    }
253
254    /// Returns all possible `EolAction` variants.
255    ///
256    /// This method is useful for generating help text, validation lists,
257    /// or iterating over all possible actions.
258    ///
259    /// # Returns
260    ///
261    /// An array containing all `EolAction` variants in declaration order.
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use cull_gmail::EolAction;
267    ///
268    /// let all_actions = EolAction::variants();
269    /// assert_eq!(all_actions.len(), 2);
270    /// assert_eq!(all_actions[0], EolAction::Trash);
271    /// assert_eq!(all_actions[1], EolAction::Delete);
272    ///
273    /// // Generate help text
274    /// println!("Available actions:");
275    /// for action in EolAction::variants() {
276    ///     println!("  {} - {}", action,
277    ///              if action.is_reversible() { "reversible" } else { "irreversible" });
278    /// }
279    /// ```
280    pub fn variants() -> &'static [EolAction] {
281        &[EolAction::Trash, EolAction::Delete]
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_default_action_is_trash() {
291        let action = EolAction::default();
292        assert_eq!(action, EolAction::Trash);
293    }
294
295    #[test]
296    fn test_copy_and_equality() {
297        let trash1 = EolAction::Trash;
298        let trash2 = trash1; // Copy semantics
299        assert_eq!(trash1, trash2);
300
301        let delete1 = EolAction::Delete;
302        let delete2 = delete1; // Copy semantics
303        assert_eq!(delete1, delete2);
304
305        assert_ne!(trash1, delete1);
306    }
307
308    #[test]
309    fn test_debug_formatting() {
310        assert_eq!(format!("{:?}", EolAction::Trash), "Trash");
311        assert_eq!(format!("{:?}", EolAction::Delete), "Delete");
312    }
313
314    #[test]
315    fn test_display_formatting() {
316        assert_eq!(EolAction::Trash.to_string(), "trash");
317        assert_eq!(EolAction::Delete.to_string(), "delete");
318        assert_eq!(format!("{}", EolAction::Trash), "trash");
319        assert_eq!(format!("{}", EolAction::Delete), "delete");
320    }
321
322    #[test]
323    fn test_parse_valid_inputs() {
324        // Test lowercase
325        assert_eq!(EolAction::parse("trash"), Some(EolAction::Trash));
326        assert_eq!(EolAction::parse("delete"), Some(EolAction::Delete));
327
328        // Test uppercase
329        assert_eq!(EolAction::parse("TRASH"), Some(EolAction::Trash));
330        assert_eq!(EolAction::parse("DELETE"), Some(EolAction::Delete));
331
332        // Test mixed case
333        assert_eq!(EolAction::parse("Trash"), Some(EolAction::Trash));
334        assert_eq!(EolAction::parse("Delete"), Some(EolAction::Delete));
335        assert_eq!(EolAction::parse("TrAsH"), Some(EolAction::Trash));
336        assert_eq!(EolAction::parse("dElEtE"), Some(EolAction::Delete));
337
338        // Test with whitespace
339        assert_eq!(EolAction::parse(" trash "), Some(EolAction::Trash));
340        assert_eq!(EolAction::parse("\tdelete\n"), Some(EolAction::Delete));
341        assert_eq!(EolAction::parse("  TRASH  "), Some(EolAction::Trash));
342    }
343
344    #[test]
345    fn test_parse_invalid_inputs() {
346        // Invalid strings
347        assert_eq!(EolAction::parse("invalid"), None);
348        assert_eq!(EolAction::parse("remove"), None);
349        assert_eq!(EolAction::parse("destroy"), None);
350        assert_eq!(EolAction::parse("archive"), None);
351
352        // Empty and whitespace
353        assert_eq!(EolAction::parse(""), None);
354        assert_eq!(EolAction::parse("   "), None);
355        assert_eq!(EolAction::parse("\t\n"), None);
356
357        // Partial matches
358        assert_eq!(EolAction::parse("tras"), None);
359        assert_eq!(EolAction::parse("delet"), None);
360        assert_eq!(EolAction::parse("trashh"), None);
361        assert_eq!(EolAction::parse("deletee"), None);
362
363        // Special characters
364        assert_eq!(EolAction::parse("trash!"), None);
365        assert_eq!(EolAction::parse("delete?"), None);
366        assert_eq!(EolAction::parse("trash-delete"), None);
367    }
368
369    #[test]
370    fn test_parse_edge_cases() {
371        // Unicode variations
372        assert_eq!(EolAction::parse("trash"), Some(EolAction::Trash)); // Unicode 't'
373
374        // Numbers and symbols
375        assert_eq!(EolAction::parse("trash123"), None);
376        assert_eq!(EolAction::parse("123delete"), None);
377        assert_eq!(EolAction::parse("t@rash"), None);
378    }
379
380    #[test]
381    fn test_is_reversible() {
382        assert!(EolAction::Trash.is_reversible());
383        assert!(!EolAction::Delete.is_reversible());
384    }
385
386    #[test]
387    fn test_variants() {
388        let variants = EolAction::variants();
389        assert_eq!(variants.len(), 2);
390        assert_eq!(variants[0], EolAction::Trash);
391        assert_eq!(variants[1], EolAction::Delete);
392
393        // Ensure all enum variants are included
394        assert!(variants.contains(&EolAction::Trash));
395        assert!(variants.contains(&EolAction::Delete));
396    }
397
398    #[test]
399    fn test_variants_completeness() {
400        // Verify that variants() returns all possible enum values
401        let variants = EolAction::variants();
402
403        // Test that we can parse back to all variants
404        for variant in variants {
405            let string_repr = variant.to_string();
406            let parsed = EolAction::parse(&string_repr);
407            assert_eq!(parsed, Some(*variant));
408        }
409    }
410
411    #[test]
412    fn test_hash_trait() {
413        use std::collections::HashMap;
414
415        let mut map = HashMap::new();
416        map.insert(EolAction::Trash, "safe");
417        map.insert(EolAction::Delete, "dangerous");
418
419        assert_eq!(map.get(&EolAction::Trash), Some(&"safe"));
420        assert_eq!(map.get(&EolAction::Delete), Some(&"dangerous"));
421    }
422
423    #[test]
424    fn test_round_trip_conversion() {
425        // Test that display -> parse -> display is consistent
426        let actions = [EolAction::Trash, EolAction::Delete];
427
428        for action in actions {
429            let string_repr = action.to_string();
430            let parsed = EolAction::parse(&string_repr).expect("Should parse successfully");
431            assert_eq!(action, parsed);
432            assert_eq!(string_repr, parsed.to_string());
433        }
434    }
435
436    #[test]
437    fn test_safety_properties() {
438        // Verify safety properties are as expected
439        assert!(
440            EolAction::Trash.is_reversible(),
441            "Trash should be reversible for safety"
442        );
443        assert!(
444            !EolAction::Delete.is_reversible(),
445            "Delete should be irreversible"
446        );
447        assert_eq!(
448            EolAction::default(),
449            EolAction::Trash,
450            "Default should be the safer option"
451        );
452    }
453
454    #[test]
455    fn test_string_case_insensitive_parsing() {
456        let test_cases = [
457            ("trash", Some(EolAction::Trash)),
458            ("TRASH", Some(EolAction::Trash)),
459            ("Trash", Some(EolAction::Trash)),
460            ("TrAsH", Some(EolAction::Trash)),
461            ("delete", Some(EolAction::Delete)),
462            ("DELETE", Some(EolAction::Delete)),
463            ("Delete", Some(EolAction::Delete)),
464            ("DeLeTe", Some(EolAction::Delete)),
465            ("invalid", None),
466            ("INVALID", None),
467            ("", None),
468        ];
469
470        for (input, expected) in test_cases {
471            assert_eq!(
472                EolAction::parse(input),
473                expected,
474                "Failed for input: '{input}'"
475            );
476        }
477    }
478
479    #[test]
480    fn test_practical_usage_scenarios() {
481        // Test common usage patterns
482
483        // Configuration parsing scenario
484        let config_value = "delete";
485        let action = EolAction::parse(config_value).unwrap_or_default();
486        assert_eq!(action, EolAction::Delete);
487
488        // Invalid config falls back to default (safe)
489        let invalid_config = "invalid_action";
490        let safe_action = EolAction::parse(invalid_config).unwrap_or_default();
491        assert_eq!(safe_action, EolAction::Trash);
492
493        // Logging/display scenario
494        let action = EolAction::Delete;
495        let log_message = format!("Executing {action} action");
496        assert_eq!(log_message, "Executing delete action");
497
498        // Safety check scenario
499        let dangerous_action = EolAction::Delete;
500        if !dangerous_action.is_reversible() {
501            // This would prompt user confirmation in real usage
502            // Test that we can detect dangerous actions
503        }
504    }
505
506    #[test]
507    fn test_error_handling_patterns() {
508        // Test error handling patterns that might be used with this enum
509
510        fn parse_with_error(input: &str) -> Result<EolAction, String> {
511            EolAction::parse(input)
512                .ok_or_else(|| format!("Invalid action: '{input}'. Valid options: trash, delete"))
513        }
514
515        // Valid cases
516        assert!(parse_with_error("trash").is_ok());
517        assert!(parse_with_error("delete").is_ok());
518
519        // Error cases
520        let error = parse_with_error("invalid").unwrap_err();
521        assert!(error.contains("Invalid action: 'invalid'"));
522        assert!(error.contains("trash, delete"));
523    }
524}