1use std::{
4 fmt::{Display, Formatter, Result as FmtResult},
5 str::FromStr,
6};
7
8use serde::{Deserialize, Serialize};
9use strum::{Display as EnumDisplay, EnumString};
10use time::{
11 format_description::well_known::{
12 iso8601::{Config as TimeFormatConfig, EncodedConfig, TimePrecision},
13 Iso8601,
14 },
15 macros::datetime,
16 Duration, OffsetDateTime,
17};
18
19#[cfg(doc)]
20use crate::stats::Statistic;
21
22#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(transparent)]
28pub struct StatisticData {
29 data: String,
30}
31
32impl From<&str> for StatisticData {
33 fn from(s: &str) -> Self {
34 Self { data: s.into() }
35 }
36}
37
38impl From<String> for StatisticData {
39 fn from(s: String) -> Self {
40 Self { data: s }
41 }
42}
43
44impl Display for StatisticData {
45 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
46 fmt.write_str(&self.data)
47 }
48}
49
50#[expect(
61 clippy::unsafe_derive_deserialize,
62 reason = "false positive in the EPOCH constant"
63)]
64#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(try_from = "&str", into = "String")]
66pub struct StatisticTime {
67 intervals: u32,
68}
69
70impl StatisticTime {
71 pub const EPOCH: OffsetDateTime = datetime!(2000-01-01 00:00:00 UTC);
73 pub const RESOLUTION_SECS: i64 = 15 * 60;
75
76 #[must_use]
78 pub fn now() -> Self {
79 let intervals =
80 (OffsetDateTime::now_utc() - Self::EPOCH).whole_seconds() / Self::RESOLUTION_SECS;
81
82 Self {
83 intervals: intervals.try_into().unwrap_or(u32::MAX),
84 }
85 }
86}
87
88impl From<OffsetDateTime> for StatisticTime {
89 fn from(dt: OffsetDateTime) -> Self {
90 let intervals = (dt - Self::EPOCH).whole_seconds() / Self::RESOLUTION_SECS;
91
92 Self {
93 intervals: intervals.try_into().unwrap_or(u32::MAX),
94 }
95 }
96}
97
98impl From<StatisticTime> for OffsetDateTime {
99 fn from(st: StatisticTime) -> OffsetDateTime {
100 let seconds = i64::from(st.intervals) * StatisticTime::RESOLUTION_SECS;
101
102 StatisticTime::EPOCH + Duration::seconds(seconds)
103 }
104}
105
106impl TryFrom<&str> for StatisticTime {
107 type Error = time::Error;
108
109 fn try_from(s: &str) -> Result<Self, Self::Error> {
110 const TIME_FORMAT_CONFIG: EncodedConfig = TimeFormatConfig::DEFAULT
111 .set_time_precision(TimePrecision::Second {
112 decimal_digits: None,
113 })
114 .encode();
115
116 let dt = OffsetDateTime::parse(s, &Iso8601::<TIME_FORMAT_CONFIG>)?;
117
118 Ok(dt.into())
119 }
120}
121
122impl FromStr for StatisticTime {
123 type Err = time::Error;
124
125 fn from_str(s: &str) -> Result<Self, Self::Err> {
126 Self::try_from(s)
127 }
128}
129
130impl From<StatisticTime> for String {
131 fn from(st: StatisticTime) -> Self {
132 st.to_string()
133 }
134}
135
136impl Display for StatisticTime {
137 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
138 let dt = OffsetDateTime::from(*self);
140
141 let (year, month, day) = dt.to_calendar_date();
142 let month = month as u8;
143
144 let (hour, minute, second) = dt.to_hms();
145
146 fmt.write_fmt(format_args!(
147 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
148 ))
149 }
150}
151
152#[derive(
157 Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, EnumDisplay,
158)]
159#[serde(rename_all = "snake_case")]
160#[strum(serialize_all = "snake_case")]
161#[non_exhaustive]
162pub enum StatisticType {
163 Request,
168 HostRequest,
177 SniRequest,
184 StatusCode,
190 HttpVersion,
196 TlsVersion,
204 TlsCipherSuite,
212 UserAgent,
226 UserAgentMobile,
235 UserAgentPlatform,
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn statistic_data() {
252 assert_eq!(
253 r#""Some Arbitrary Text Data""#,
254 serde_json::to_string_pretty(&StatisticData::from("Some Arbitrary Text Data")).unwrap()
255 );
256
257 assert_eq!(
258 serde_json::from_str::<StatisticData>(r#""!@#$%^&*()\" ᓚᘏᗢ""#).unwrap(),
259 StatisticData::from(r#"!@#$%^&*()" ᓚᘏᗢ"#)
260 );
261
262 assert_ne!(
263 r#""slightly different""#,
264 serde_json::to_string_pretty(&StatisticData::from("hardly different")).unwrap()
265 );
266
267 assert_eq!(
268 "Some Arbitrary Text Data",
269 StatisticData::from("Some Arbitrary Text Data").to_string()
270 );
271 }
272
273 #[test]
274 fn statistic_time() {
275 assert_ne!(StatisticTime::now(), StatisticTime::EPOCH.into());
276
277 assert_eq!(
278 StatisticTime::try_from(StatisticTime::now().to_string().as_str()).unwrap(),
279 StatisticTime::now()
280 );
281
282 assert_eq!(
283 StatisticTime::try_from("2022-10-08T16:30:00Z").unwrap(),
284 StatisticTime::try_from("2022-10-08T16:34:25.159Z").unwrap()
285 );
286
287 assert_ne!(
288 OffsetDateTime::from(StatisticTime::try_from("2022-10-08T16:34:25.159Z").unwrap()),
289 datetime!(2022-10-08 16:34:25.159 UTC)
290 );
291
292 assert_eq!(
293 OffsetDateTime::from(StatisticTime::try_from("2022-10-08T16:34:25.159Z").unwrap()),
294 datetime!(2022-10-08 16:30:00.000 UTC)
295 );
296
297 assert!(dbg!(StatisticTime::now().to_string()).ends_with(":00Z"));
298 assert_eq!(dbg!(StatisticTime::now().to_string()).len(), 20);
299
300 let stat_time = StatisticTime::from(datetime!(2022-09-30 15:24:38 +2));
301
302 assert_eq!(dbg!(stat_time.to_string()), "2022-09-30T13:15:00Z");
303 assert_eq!(
304 stat_time,
305 StatisticTime::try_from(stat_time.to_string().as_str()).unwrap()
306 );
307 assert_eq!(
308 stat_time,
309 serde_json::from_str(&serde_json::to_string(&stat_time).unwrap()).unwrap()
310 );
311
312 let stat_time = StatisticTime::now();
313
314 assert_eq!(
315 stat_time,
316 StatisticTime::try_from(stat_time.to_string().as_str()).unwrap()
317 );
318 assert_eq!(
319 stat_time,
320 serde_json::from_str(&serde_json::to_string(&stat_time).unwrap()).unwrap()
321 );
322 }
323
324 #[test]
325 fn statistic_type() {
326 assert_eq!(
327 StatisticType::HostRequest,
328 serde_json::from_str(
329 &serde_json::to_string_pretty(&StatisticType::HostRequest).unwrap()
330 )
331 .unwrap()
332 );
333
334 assert_eq!(
335 r#""user_agent_platform""#,
336 serde_json::to_string_pretty(&StatisticType::UserAgentPlatform).unwrap()
337 );
338
339 assert_eq!(
340 StatisticType::HttpVersion,
341 serde_json::from_str(r#""http_version""#).unwrap()
342 );
343
344 assert!(serde_json::from_str::<StatisticType>(r#""an_invalid_type""#).is_err());
345 }
346}