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 => {
61 f.write_str("The value contains escape codes but it does not need them.")
62 }
63 WarningKind::Decode(warning) => fmt::Display::fmt(warning, f),
64 WarningKind::Invalid(err) => write!(f, "The value is not valid: {err}"),
65 WarningKind::InvalidType => write!(f, "The value should be a string."),
66 }
67 }
68}
69
70impl warning::Kind for WarningKind {
71 fn id(&self) -> std::borrow::Cow<'static, str> {
72 match self {
73 WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
74 WarningKind::Decode(kind) => kind.id(),
75 WarningKind::Invalid(_) => "invalid".into(),
76 WarningKind::InvalidType => "invalid_type".into(),
77 }
78 }
79}
80
81impl From<json::decode::WarningKind> for WarningKind {
82 fn from(warn_kind: json::decode::WarningKind) -> Self {
83 Self::Decode(warn_kind)
84 }
85}
86
87into_caveat!(DateTime<Utc>);
88into_caveat_all!(chrono::NaiveTime, chrono::NaiveDate);
89
90impl json::FromJson<'_, '_> for DateTime<Utc> {
91 type WarningKind = WarningKind;
92
93 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
94 let mut warnings = warning::Set::new();
95 let Some(s) = elem.as_raw_str() else {
96 return warnings.bail(WarningKind::InvalidType, elem);
97 };
98
99 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
100
101 let s = match pending_str {
102 json::decode::PendingStr::NoEscapes(s) => s,
103 json::decode::PendingStr::HasEscapes(_) => {
104 return warnings.bail(WarningKind::ContainsEscapeCodes, elem);
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 return warnings.bail(WarningKind::Invalid(err.to_string()), elem);
116 };
117
118 let datetime = Utc.from_utc_datetime(&date);
119 Ok(datetime.into_caveat(warnings))
120 }
121}
122
123impl json::FromJson<'_, '_> for chrono::NaiveDate {
124 type WarningKind = WarningKind;
125
126 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
127 let mut warnings = warning::Set::new();
128 let Some(s) = elem.as_raw_str() else {
129 return warnings.bail(WarningKind::InvalidType, elem);
130 };
131
132 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
133
134 let s = match pending_str {
135 json::decode::PendingStr::NoEscapes(s) => s,
136 json::decode::PendingStr::HasEscapes(_) => {
137 return warnings.bail(WarningKind::ContainsEscapeCodes, elem);
138 }
139 };
140
141 let date = match s.parse::<chrono::NaiveDate>() {
142 Ok(v) => v,
143 Err(err) => {
144 return warnings.bail(WarningKind::Invalid(err.to_string()), elem);
145 }
146 };
147
148 Ok(date.into_caveat(warnings))
149 }
150}
151
152impl json::FromJson<'_, '_> for chrono::NaiveTime {
153 type WarningKind = WarningKind;
154
155 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
156 let mut warnings = warning::Set::new();
157 let value = elem.as_value();
158
159 let Some(s) = value.as_raw_str() else {
160 return warnings.bail(WarningKind::InvalidType, elem);
161 };
162
163 let pending_str = s.has_escapes(elem).gather_warnings_into(&mut warnings);
164
165 let s = match pending_str {
166 json::decode::PendingStr::NoEscapes(s) => s,
167 json::decode::PendingStr::HasEscapes(_) => {
168 return warnings.bail(WarningKind::ContainsEscapeCodes, elem);
169 }
170 };
171
172 let date = match chrono::NaiveTime::parse_from_str(s, "%H:%M") {
173 Ok(v) => v,
174 Err(err) => {
175 return warnings.bail(WarningKind::Invalid(err.to_string()), elem);
176 }
177 };
178
179 Ok(date.into_caveat(warnings))
180 }
181}
182
183#[cfg(test)]
184pub mod test {
185 use chrono::{DateTime, NaiveDateTime, TimeZone as _, Utc};
186
187 use crate::test::ApproxEq;
188
189 impl ApproxEq for DateTime<Utc> {
190 fn approx_eq(&self, other: &Self) -> bool {
191 const EQ_TOLERANCE: i64 = 2;
193
194 let diff = *self - *other;
195 diff.num_seconds().abs() <= EQ_TOLERANCE
196 }
197 }
198
199 pub fn deser_to_utc<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
201 where
202 D: serde::Deserializer<'de>,
203 {
204 use serde::Deserialize;
205
206 let date_string = String::deserialize(deserializer)?;
207
208 let err = match date_string.parse::<DateTime<Utc>>() {
210 Ok(date) => return Ok(date),
211 Err(err) => err,
212 };
213
214 if let Ok(date) = date_string.parse::<NaiveDateTime>() {
215 Ok(Utc.from_utc_datetime(&date))
216 } else {
217 Err(serde::de::Error::custom(err))
218 }
219 }
220}
221
222#[cfg(test)]
223mod test_datetime_from_json {
224 #![allow(
225 clippy::unwrap_in_result,
226 reason = "unwraps are allowed anywhere in tests"
227 )]
228
229 use assert_matches::assert_matches;
230 use chrono::{DateTime, TimeZone, Utc};
231
232 use crate::{
233 json::{self, FromJson as _},
234 Verdict,
235 };
236
237 use super::WarningKind;
238
239 #[track_caller]
240 fn parse_timestamp_from_json(json: &'static str) -> Verdict<DateTime<Utc>, WarningKind> {
241 let elem = json::parse(json).unwrap();
242 let date_time_time = elem.find_field("start_date_time").unwrap();
243 DateTime::<Utc>::from_json(date_time_time.element())
244 }
245
246 #[test]
247 fn should_parse_utc_datetime() {
248 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09Z" }"#;
249
250 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
251 assert_matches!(*warnings, []);
252 assert_eq!(
253 datetime,
254 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
255 );
256 }
257
258 #[test]
259 fn should_parse_timezone_to_utc() {
260 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09+02:00" }"#;
261
262 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
263 assert_matches!(*warnings, []);
264 assert_eq!(
265 datetime,
266 Utc.with_ymd_and_hms(2015, 6, 29, 20, 39, 9).unwrap()
267 );
268 }
269
270 #[test]
271 fn should_parse_timezone_naive_to_utc() {
272 const JSON: &str = r#"{ "start_date_time": "2015-06-29T22:39:09" }"#;
275
276 let (datetime, warnings) = parse_timestamp_from_json(JSON).unwrap().into_parts();
277 assert_matches!(*warnings, []);
278
279 assert_eq!(
280 datetime,
281 Utc.with_ymd_and_hms(2015, 6, 29, 22, 39, 9).unwrap()
282 );
283 }
284}