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