1use std::fmt;
17
18use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};
19
20use crate::{
21 into_caveat, into_caveat_all, 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) => kind.id(),
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>);
86into_caveat_all!(chrono::NaiveTime, chrono::NaiveDate);
87
88impl json::FromJson<'_, '_> for DateTime<Utc> {
89 type WarningKind = WarningKind;
90
91 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
92 let mut warnings = warning::Set::new();
93 let Some(s) = elem.as_raw_str() else {
94 warnings.with_elem(WarningKind::InvalidType, elem);
95 return Err(warnings);
96 };
97
98 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
99
100 let s = match pending_str {
101 json::decode::PendingStr::NoEscapes(s) => s,
102 json::decode::PendingStr::HasEscapes(_) => {
103 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
104 return Err(warnings);
105 }
106 };
107
108 let err = match s.parse::<DateTime<Utc>>() {
110 Ok(date) => return Ok(date.into_caveat(warnings)),
111 Err(err) => err,
112 };
113
114 let Ok(date) = s.parse::<NaiveDateTime>() else {
115 warnings.with_elem(WarningKind::Invalid(err.to_string()), elem);
116 return Err(warnings);
117 };
118
119 let datetime = Utc.from_utc_datetime(&date);
120 Ok(datetime.into_caveat(warnings))
121 }
122}
123
124impl json::FromJson<'_, '_> for chrono::NaiveDate {
125 type WarningKind = WarningKind;
126
127 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
128 let mut warnings = warning::Set::new();
129 let Some(s) = elem.as_raw_str() else {
130 warnings.with_elem(WarningKind::InvalidType, elem);
131 return Err(warnings);
132 };
133
134 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
135
136 let s = match pending_str {
137 json::decode::PendingStr::NoEscapes(s) => s,
138 json::decode::PendingStr::HasEscapes(_) => {
139 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
140 return Err(warnings);
141 }
142 };
143
144 let date = match s.parse::<chrono::NaiveDate>() {
145 Ok(v) => v,
146 Err(err) => {
147 warnings.with_elem(WarningKind::Invalid(err.to_string()), elem);
148 return Err(warnings);
149 }
150 };
151
152 Ok(date.into_caveat(warnings))
153 }
154}
155
156impl json::FromJson<'_, '_> for chrono::NaiveTime {
157 type WarningKind = WarningKind;
158
159 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
160 let mut warnings = warning::Set::new();
161 let value = elem.as_value();
162
163 let Some(s) = value.as_raw_str() else {
164 warnings.with_elem(WarningKind::InvalidType, elem);
165 return Err(warnings);
166 };
167
168 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
169
170 let s = match pending_str {
171 json::decode::PendingStr::NoEscapes(s) => s,
172 json::decode::PendingStr::HasEscapes(_) => {
173 warnings.with_elem(WarningKind::ContainsEscapeCodes, elem);
174 return Err(warnings);
175 }
176 };
177
178 let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
179 Ok(v) => v,
180 Err(err) => {
181 warnings.with_elem(WarningKind::Invalid(err.to_string()), elem);
182 return Err(warnings);
183 }
184 };
185
186 Ok(date.into_caveat(warnings))
187 }
188}
189
190#[cfg(test)]
191pub mod test {
192 use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};
193
194 use crate::test::ApproxEq;
195
196 impl ApproxEq for DateTime<Utc> {
197 fn approx_eq(&self, other: &Self) -> bool {
198 const EQ_TOLERANCE: i64 = 2;
200
201 let diff = *self - *other;
202 diff.num_seconds().abs() <= EQ_TOLERANCE
203 }
204 }
205
206 pub fn deser_to_utc<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
208 where
209 D: serde::Deserializer<'de>,
210 {
211 use serde::Deserialize;
212
213 let date_string = String::deserialize(deserializer)?;
214
215 let err = match date_string.parse::<DateTime<Utc>>() {
217 Ok(date) => return Ok(date),
218 Err(err) => err,
219 };
220
221 if let Ok(date) = date_string.parse::<NaiveDateTime>() {
222 Ok(Utc.from_utc_datetime(&date))
223 } else {
224 Err(serde::de::Error::custom(err))
225 }
226 }
227}
228
229#[cfg(test)]
230mod test_datetime_from_json {
231 #![allow(
232 clippy::unwrap_in_result,
233 reason = "unwraps are allowed anywhere in tests"
234 )]
235
236 use assert_matches::assert_matches;
237 use chrono::{DateTime, TimeZone, Utc};
238
239 use crate::{
240 json::{self, FromJson as _},
241 Verdict,
242 };
243
244 use super::WarningKind;
245
246 #[track_caller]
247 fn parse_timestamp_from_json(json: &'static str) -> Verdict<DateTime<Utc>, WarningKind> {
248 let elem = json::parse(json).unwrap();
249 let date_time_time = elem.find_field("start_date_time").unwrap();
250 DateTime::<Utc>::from_json(date_time_time.element())
251 }
252
253 #[test]
254 fn should_parse_utc_datetime() {
255 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09Z" }"#;
256
257 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
258 assert_matches!(*warnings, []);
259 assert_eq!(
260 datetime,
261 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
262 );
263 }
264
265 #[test]
266 fn should_parse_timezone_to_utc() {
267 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09+02:00" }"#;
268
269 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
270 assert_matches!(*warnings, []);
271 assert_eq!(
272 datetime,
273 Utc.with_ymd_and_hms(2015, 6, 29, 20, 39, 9).unwrap()
274 );
275 }
276
277 #[test]
278 fn should_parse_timezone_naive_to_utc() {
279 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09" }"#;
282
283 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
284 assert_matches!(*warnings, []);
285
286 assert_eq!(
287 datetime,
288 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
289 );
290 }
291}