1use std::fmt;
17
18use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};
19
20use crate::{
21 into_caveat, json,
22 warning::{self, GatherWarnings as _},
23 IntoCaveat, Verdict,
24};
25
26#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
28pub enum WarningKind {
29 ContainsEscapeCodes,
31
32 Decode(json::decode::WarningKind),
34
35 Invalid(String),
52
53 InvalidType,
55}
56
57impl fmt::Display for WarningKind {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 WarningKind::ContainsEscapeCodes => write!(f, "contains_escape_codes"),
61 WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
62 WarningKind::Invalid(_) => write!(f, "invalid"),
63 WarningKind::InvalidType => write!(f, "invalid_type"),
64 }
65 }
66}
67
68impl warning::Kind for WarningKind {
69 fn id(&self) -> std::borrow::Cow<'static, str> {
70 match self {
71 WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
72 WarningKind::Decode(kind) => format!("decode.{}", kind.id()).into(),
73 WarningKind::Invalid(_) => "invalid".into(),
74 WarningKind::InvalidType => "invalid_type".into(),
75 }
76 }
77}
78
79impl From<json::decode::WarningKind> for WarningKind {
80 fn from(warn_kind: json::decode::WarningKind) -> Self {
81 Self::Decode(warn_kind)
82 }
83}
84
85into_caveat!(DateTime<Utc>);
86
87pub(crate) fn deser_to_utc<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
91where
92 D: serde::Deserializer<'de>,
93{
94 use serde::Deserialize;
95
96 let date_string = String::deserialize(deserializer)?;
97
98 let err = match date_string.parse::<DateTime<Utc>>() {
100 Ok(date) => return Ok(date),
101 Err(err) => err,
102 };
103
104 if let Ok(date) = date_string.parse::<NaiveDateTime>() {
105 Ok(Utc.from_utc_datetime(&date))
106 } else {
107 Err(serde::de::Error::custom(err))
108 }
109}
110
111impl json::FromJson<'_, '_> for DateTime<Utc> {
112 type WarningKind = WarningKind;
113
114 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
115 let mut warnings = warning::Set::new();
116 let Some(s) = elem.as_raw_str() else {
117 warnings.with_elem(WarningKind::InvalidType, elem);
118 return Err(warnings);
119 };
120
121 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
122
123 let s = match pending_str {
124 json::decode::PendingStr::NoEscapes(s) => s,
125 json::decode::PendingStr::HasEscapes(_) => {
126 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
127 return Err(warnings);
128 }
129 };
130
131 let err = match s.parse::<DateTime<Utc>>() {
133 Ok(date) => return Ok(date.into_caveat(warnings)),
134 Err(err) => err,
135 };
136
137 let Ok(date) = s.parse::<NaiveDateTime>() else {
138 warnings.with_elem(WarningKind::Invalid(err.to_string()), elem);
139 return Err(warnings);
140 };
141
142 let datetime = Utc.from_utc_datetime(&date);
143 Ok(datetime.into_caveat(warnings))
144 }
145}
146
147into_caveat!(chrono::NaiveDate);
148
149impl json::FromJson<'_, '_> for chrono::NaiveDate {
150 type WarningKind = WarningKind;
151
152 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
153 let mut warnings = warning::Set::new();
154 let Some(s) = elem.as_raw_str() else {
155 warnings.with_elem(WarningKind::InvalidType, elem);
156 return Err(warnings);
157 };
158
159 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
160
161 let s = match pending_str {
162 json::decode::PendingStr::NoEscapes(s) => s,
163 json::decode::PendingStr::HasEscapes(_) => {
164 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
165 return Err(warnings);
166 }
167 };
168
169 let date = match s.parse::<chrono::NaiveDate>() {
170 Ok(v) => v,
171 Err(err) => {
172 warnings.with_elem(WarningKind::Invalid(err.to_string()), elem);
173 return Err(warnings);
174 }
175 };
176
177 Ok(date.into_caveat(warnings))
178 }
179}
180
181into_caveat!(chrono::NaiveTime);
182
183impl json::FromJson<'_, '_> for chrono::NaiveTime {
184 type WarningKind = WarningKind;
185
186 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
187 let mut warnings = warning::Set::new();
188 let value = elem.as_value();
189
190 let Some(s) = value.as_raw_str() else {
191 warnings.with_elem(WarningKind::InvalidType, elem);
192 return Err(warnings);
193 };
194
195 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
196
197 let s = match pending_str {
198 json::decode::PendingStr::NoEscapes(s) => s,
199 json::decode::PendingStr::HasEscapes(_) => {
200 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
201 return Err(warnings);
202 }
203 };
204
205 let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
206 Ok(v) => v,
207 Err(err) => {
208 warnings.with_elem(WarningKind::Invalid(err.to_string()), elem);
209 return Err(warnings);
210 }
211 };
212
213 Ok(date.into_caveat(warnings))
214 }
215}
216
217#[cfg(test)]
218mod test {
219 use chrono::{DateTime, Utc};
220
221 use crate::test::ApproxEq;
222
223 impl ApproxEq for DateTime<Utc> {
224 fn approx_eq(&self, other: &Self) -> bool {
225 const EQ_TOLERANCE: i64 = 2;
227
228 let diff = *self - *other;
229 diff.num_seconds().abs() <= EQ_TOLERANCE
230 }
231 }
232}
233
234#[cfg(test)]
235mod test_datetime_serde_deser {
236 use chrono::{DateTime, TimeZone, Utc};
237 use serde::de::{value::StrDeserializer, IntoDeserializer as _};
238
239 use super::deser_to_utc;
240
241 #[track_caller]
242 fn parse_timestamp(timestamp: &str) -> DateTime<Utc> {
243 let de: StrDeserializer<'_, serde::de::value::Error> = timestamp.into_deserializer();
244 deser_to_utc(de).unwrap()
245 }
246
247 #[test]
248 fn should_parse_utc_datetime() {
249 assert_eq!(
250 parse_timestamp("2015-06-29T22:39:09Z"),
251 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
252 );
253 }
254
255 #[test]
256 fn should_parse_timezone_to_utc() {
257 assert_eq!(
258 parse_timestamp("2015-06-29T22:39:09+02:00"),
259 Utc.with_ymd_and_hms(2015, 6, 29, 20, 39, 9).unwrap()
260 );
261 }
262
263 #[test]
264 fn should_parse_timezone_naive_to_utc() {
265 assert_eq!(
268 parse_timestamp("2015-06-29T22:39:09"),
269 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
270 );
271 }
272}
273
274#[cfg(test)]
275mod test_datetime_from_json {
276 #![allow(
277 clippy::unwrap_in_result,
278 reason = "unwraps are allowed anywhere in tests"
279 )]
280
281 use assert_matches::assert_matches;
282 use chrono::{DateTime, TimeZone, Utc};
283
284 use crate::{
285 json::{self, FromJson as _},
286 Verdict,
287 };
288
289 use super::WarningKind;
290
291 #[track_caller]
292 fn parse_timestamp_from_json(json: &'static str) -> Verdict<DateTime<Utc>, WarningKind> {
293 let elem = json::parse(json).unwrap();
294 let date_time_time = elem.find_field("start_date_time").unwrap();
295 DateTime::<Utc>::from_json(date_time_time.element())
296 }
297
298 #[test]
299 fn should_parse_utc_datetime() {
300 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09Z" }"#;
301
302 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
303 assert_matches!(*warnings, []);
304 assert_eq!(
305 datetime,
306 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
307 );
308 }
309
310 #[test]
311 fn should_parse_timezone_to_utc() {
312 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09+02:00" }"#;
313
314 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
315 assert_matches!(*warnings, []);
316 assert_eq!(
317 datetime,
318 Utc.with_ymd_and_hms(2015, 6, 29, 20, 39, 9).unwrap()
319 );
320 }
321
322 #[test]
323 fn should_parse_timezone_naive_to_utc() {
324 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09" }"#;
327
328 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
329 assert_matches!(*warnings, []);
330
331 assert_eq!(
332 datetime,
333 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
334 );
335 }
336}