Skip to main content

walnut_dns/rr/
ttl.rs

1use std::fmt;
2use std::time::Duration;
3
4use chrono::Utc;
5
6use rusqlite::types::{FromSql, ToSql};
7
8/// DNS Cache Time-to-live, in seconds
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub struct TimeToLive(u32);
11
12impl TimeToLive {
13    pub const MAX: TimeToLive = TimeToLive(u32::MAX);
14    pub const MIN: TimeToLive = TimeToLive(u32::MIN);
15    pub const ZERO: TimeToLive = TimeToLive(0u32);
16
17    /// Create a TimeToLive from seconds
18    ///
19    /// Creates a new TimeToLive value from the specified number of seconds.
20    ///
21    /// # Arguments
22    ///
23    /// * `secs` - The TTL value in seconds
24    ///
25    /// # Returns
26    ///
27    /// A new TimeToLive instance
28    pub fn from_secs(secs: u32) -> Self {
29        TimeToLive(secs)
30    }
31
32    /// Calculate the deadline for this TTL
33    ///
34    /// Returns the UTC timestamp when a record with this TTL would expire
35    /// if cached now.
36    ///
37    /// # Returns
38    ///
39    /// The expiration timestamp
40    pub fn deadline(&self) -> chrono::DateTime<Utc> {
41        Utc::now() + Duration::from(*self)
42    }
43}
44
45impl fmt::Display for TimeToLive {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "{}", self.0)
48    }
49}
50
51impl From<u32> for TimeToLive {
52    fn from(value: u32) -> Self {
53        TimeToLive(value)
54    }
55}
56
57impl From<TimeToLive> for u32 {
58    fn from(value: TimeToLive) -> Self {
59        value.0
60    }
61}
62
63impl From<Duration> for TimeToLive {
64    fn from(value: Duration) -> Self {
65        TimeToLive(
66            value
67                .as_secs()
68                .try_into()
69                .expect("Duration does not fit in u32 seconds"),
70        )
71    }
72}
73
74impl From<TimeToLive> for Duration {
75    fn from(value: TimeToLive) -> Self {
76        Duration::from_secs(value.0 as u64)
77    }
78}
79
80impl ToSql for TimeToLive {
81    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
82        self.0.to_sql()
83    }
84}
85
86impl FromSql for TimeToLive {
87    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
88        u32::column_result(value).map(TimeToLive)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use std::time::Duration;
96
97    #[test]
98    fn test_ttl_constants() {
99        assert_eq!(TimeToLive::ZERO.0, 0);
100        assert_eq!(TimeToLive::MIN.0, u32::MIN);
101        assert_eq!(TimeToLive::MAX.0, u32::MAX);
102    }
103
104    #[test]
105    fn test_ttl_from_secs() {
106        let ttl = TimeToLive::from_secs(3600);
107        assert_eq!(ttl.0, 3600);
108    }
109
110    #[test]
111    fn test_ttl_from_u32() {
112        let ttl = TimeToLive::from(300);
113        assert_eq!(ttl.0, 300);
114
115        let value: u32 = ttl.into();
116        assert_eq!(value, 300);
117    }
118
119    #[test]
120    fn test_ttl_from_duration() {
121        let duration = Duration::from_secs(1800);
122        let ttl = TimeToLive::from(duration);
123        assert_eq!(ttl.0, 1800);
124
125        let back_to_duration: Duration = ttl.into();
126        assert_eq!(back_to_duration, duration);
127    }
128
129    #[test]
130    #[should_panic(expected = "Duration does not fit in u32 seconds")]
131    fn test_ttl_from_duration_overflow() {
132        let duration = Duration::from_secs(u64::MAX);
133        let _ttl = TimeToLive::from(duration);
134    }
135
136    #[test]
137    fn test_ttl_display() {
138        let ttl = TimeToLive::from(3600);
139        assert_eq!(format!("{ttl}"), "3600");
140    }
141
142    #[test]
143    fn test_ttl_ordering() {
144        let ttl1 = TimeToLive::from(60);
145        let ttl2 = TimeToLive::from(120);
146        let ttl3 = TimeToLive::from(60);
147
148        assert!(ttl1 < ttl2);
149        assert!(ttl2 > ttl1);
150        assert_eq!(ttl1, ttl3);
151        assert!(ttl1 <= ttl3);
152        assert!(ttl1 >= ttl3);
153    }
154
155    #[test]
156    fn test_ttl_deadline() {
157        let ttl = TimeToLive::from(3600); // 1 hour
158        let deadline = ttl.deadline();
159        let now = Utc::now();
160
161        // Deadline should be roughly 1 hour from now (within a few seconds for test execution time)
162        let diff = deadline - now;
163        assert!(diff.num_seconds() >= 3590); // At least 59:50
164        assert!(diff.num_seconds() <= 3610); // At most 60:10
165    }
166
167    #[test]
168    fn test_ttl_clone_and_copy() {
169        let ttl1 = TimeToLive::from(300);
170        let ttl2 = ttl1; // Copy
171        let ttl3 = ttl1; // Clone
172
173        assert_eq!(ttl1, ttl2);
174        assert_eq!(ttl1, ttl3);
175        assert_eq!(ttl1.0, 300);
176        assert_eq!(ttl2.0, 300);
177        assert_eq!(ttl3.0, 300);
178    }
179
180    #[test]
181    fn test_ttl_zero_deadline() {
182        let ttl = TimeToLive::ZERO;
183        let deadline = ttl.deadline();
184        let now = Utc::now();
185
186        // Zero TTL should have deadline very close to now
187        let diff = deadline - now;
188        assert!(diff.num_seconds() >= -10);
189        assert!(diff.num_seconds() <= 10);
190    }
191
192    #[test]
193    fn test_ttl_debug() {
194        let ttl = TimeToLive::from(600);
195        let debug_str = format!("{ttl:?}");
196        assert!(debug_str.contains("TimeToLive"));
197        assert!(debug_str.contains("600"));
198    }
199}