1use std::fmt;
2use std::time::Duration;
3
4use chrono::Utc;
5
6use rusqlite::types::{FromSql, ToSql};
7
8#[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 pub fn from_secs(secs: u32) -> Self {
29 TimeToLive(secs)
30 }
31
32 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); let deadline = ttl.deadline();
159 let now = Utc::now();
160
161 let diff = deadline - now;
163 assert!(diff.num_seconds() >= 3590); assert!(diff.num_seconds() <= 3610); }
166
167 #[test]
168 fn test_ttl_clone_and_copy() {
169 let ttl1 = TimeToLive::from(300);
170 let ttl2 = ttl1; let ttl3 = ttl1; 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 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}