xapi_rs/data/
timestamp.rs1use crate::{
4 data::{DataError, ValidationError},
5 emit_error,
6};
7use chrono::{DateTime, SecondsFormat, Utc};
8use core::fmt;
9use serde_with::{DeserializeFromStr, SerializeDisplay};
10use std::str::FromStr;
11use tracing::error;
12
13#[derive(Clone, Debug, DeserializeFromStr, PartialEq, SerializeDisplay)]
15pub struct MyTimestamp(DateTime<Utc>);
16
17impl MyTimestamp {
18 pub fn from(inner: DateTime<Utc>) -> Self {
20 MyTimestamp(inner)
21 }
22
23 pub fn inner(&self) -> &DateTime<Utc> {
25 &self.0
26 }
27}
28
29impl FromStr for MyTimestamp {
30 type Err = DataError;
31
32 fn from_str(s: &str) -> Result<Self, Self::Err> {
33 let s = s.trim();
34 let parsed = DateTime::parse_from_rfc3339(s).map_err(|x| {
35 error!("Failed parse '{}' as an RFC-3339 date-time: {}", s, x);
36 DataError::Time(x)
37 })?;
38 let offset_seconds = parsed.offset().local_minus_utc();
39 if offset_seconds == 0 && (s.ends_with("-00:00") || s.ends_with("-0000")) {
40 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
41 "negative 0 offset".into()
42 )))
43 }
44
45 Ok(MyTimestamp(parsed.with_timezone(&Utc)))
46 }
47}
48
49impl fmt::Display for MyTimestamp {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Millis, true))
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use serde::{Deserialize, Serialize};
59 use tracing_test::traced_test;
60
61 #[derive(Debug, Deserialize, Serialize)]
62 struct Foo {
63 ts: Option<MyTimestamp>,
64 }
65
66 #[traced_test]
67 #[test]
68 fn test_good_timestamps() -> Result<(), DataError> {
69 const OK1: &str = "2024-09-19T12:05:13+00:00";
70 const F1: &str = r#"{"ts":"2024-09-19T12:05:13.000+00:00"}"#;
71 const OK2: &str = "2024-09-19T12:05:13.000Z";
72
73 let x = MyTimestamp::from_str(OK1)?;
74 let f: Foo = serde_json::from_str(F1)?;
75 let y = f.ts.unwrap();
76 assert_eq!(x, y);
77 let out = x.to_string();
78 assert_eq!(out, OK2);
79
80 const F2: &str = r#"{"ts":"2024-10-19T12:05:13+00:00"}"#;
81 const OK3: &str = "2024-10-19T12:05:13.000Z";
82 let f: Foo = serde_json::from_str(F2)?;
83 let out = serde_json::to_string(&f)?;
84 assert_eq!(out, format!("{{\"ts\":\"{}\"}}", OK3));
85
86 const F3: &str = r#"{"ts":"2023-10-01T12:00:00-05:00"}"#;
88 const OK4: &str = "2023-10-01T17:00:00.000Z";
89
90 let f = serde_json::from_str::<Foo>(F3)?;
91 let x = MyTimestamp::from_str(OK4)?;
92 assert_eq!(Some(x), f.ts);
93 let out = serde_json::to_string(&f)?;
94 assert_eq!(out, format!("{{\"ts\":\"{}\"}}", OK4));
95
96 Ok(())
97 }
98
99 #[traced_test]
100 #[test]
101 fn test_reject_invalid() {
102 const TS: &str = "2008-09-15T15:53:00.601-0000";
103
104 assert!(serde_json::from_str::<MyTimestamp>(TS).is_err());
105 assert!(MyTimestamp::from_str(TS).is_err());
106 }
107
108 #[traced_test]
109 #[test]
110 fn test_negative_zero_offset() {
111 const T1: &str = "2008-09-15T15:53:00.601-0000";
112 const T2: &str = "2008-09-15T15:53:00.601-00:00";
113
114 assert!(serde_json::from_str::<MyTimestamp>(T1).is_err());
115 assert!(serde_json::from_str::<MyTimestamp>(T2).is_err());
116
117 assert!(MyTimestamp::from_str(T1).is_err());
118 assert!(MyTimestamp::from_str(T2).is_err());
119 }
120
121 #[traced_test]
122 #[test]
123 fn test_invalid_formats() {
124 const BAD1: &str = "";
125 const BAD2: &str = "foo";
126 const BAD3: &str = "2015-11-18T12";
127 const BAD4: &str = "2015-11-18T12:17:00";
128
129 assert!(MyTimestamp::from_str(BAD1).is_err());
130 assert!(MyTimestamp::from_str(BAD2).is_err());
131 assert!(MyTimestamp::from_str(BAD3).is_err());
132 assert!(MyTimestamp::from_str(BAD4).is_err());
133 }
134}