Skip to main content

torrust_tracker_deployer_lib/domain/backup/
retention_days.rs

1//! Backup retention period in days.
2
3use std::num::NonZeroU32;
4
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8/// Number of days to retain backups before deletion.
9///
10/// Must be at least 1 day. Values of 0 are rejected to prevent
11/// accidental deletion of all backups.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
13pub struct RetentionDays(NonZeroU32);
14
15/// Errors that can occur when creating `RetentionDays`.
16#[derive(Debug, Error, PartialEq, Eq)]
17pub enum RetentionDaysError {
18    /// Retention days must be at least 1
19    #[error("Retention days must be at least 1 (got 0)")]
20    Zero,
21}
22
23impl RetentionDays {
24    /// Creates a new retention period.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if `days` is 0.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use torrust_tracker_deployer_lib::domain::backup::RetentionDays;
34    ///
35    /// let retention = RetentionDays::new(7)?;
36    /// assert_eq!(retention.as_u32(), 7);
37    /// # Ok::<(), Box<dyn std::error::Error>>(())
38    /// ```
39    pub fn new(days: u32) -> Result<Self, RetentionDaysError> {
40        NonZeroU32::new(days)
41            .map(Self)
42            .ok_or(RetentionDaysError::Zero)
43    }
44
45    /// Returns the retention period as a u32.
46    #[must_use]
47    pub const fn as_u32(self) -> u32 {
48        self.0.get()
49    }
50}
51
52impl Default for RetentionDays {
53    /// Default retention: 7 days
54    fn default() -> Self {
55        Self(NonZeroU32::new(7).expect("7 is non-zero"))
56    }
57}
58
59impl<'de> Deserialize<'de> for RetentionDays {
60    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61    where
62        D: serde::Deserializer<'de>,
63    {
64        let days = u32::deserialize(deserializer)?;
65        Self::new(days).map_err(serde::de::Error::custom)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use rstest::rstest;
72
73    use super::*;
74
75    #[rstest]
76    #[case(1)]
77    #[case(7)]
78    #[case(14)]
79    #[case(30)]
80    #[case(90)]
81    #[case(365)]
82    fn it_should_accept_valid_retention_days(#[case] days: u32) {
83        let result = RetentionDays::new(days);
84        assert!(result.is_ok(), "Retention days {days} should be valid");
85        assert_eq!(result.unwrap().as_u32(), days);
86    }
87
88    #[test]
89    fn it_should_reject_zero_days() {
90        let result = RetentionDays::new(0);
91        assert_eq!(result, Err(RetentionDaysError::Zero));
92    }
93
94    #[test]
95    fn it_should_use_sensible_default() {
96        let retention = RetentionDays::default();
97        assert_eq!(retention.as_u32(), 7);
98    }
99
100    #[test]
101    fn it_should_deserialize_valid_retention_days() {
102        let json = "7";
103        let retention: RetentionDays = serde_json::from_str(json).expect("valid retention");
104        assert_eq!(retention.as_u32(), 7);
105    }
106
107    #[test]
108    fn it_should_reject_zero_during_deserialization() {
109        let json = "0";
110        let result: Result<RetentionDays, _> = serde_json::from_str(json);
111        assert!(result.is_err(), "Zero should fail deserialization");
112    }
113
114    #[test]
115    fn it_should_serialize_and_deserialize_correctly() {
116        let original = RetentionDays::new(7).expect("valid retention");
117        let json = serde_json::to_string(&original).expect("serialization should succeed");
118        let deserialized: RetentionDays =
119            serde_json::from_str(&json).expect("deserialization should succeed");
120
121        assert_eq!(original, deserialized);
122    }
123}