expect_json/expects/ops/
expect_iso_date_time.rs1use crate::expect_op;
2use crate::ops::utils::DurationFormatter;
3use crate::Context;
4use crate::ExpectOp;
5use crate::ExpectOpError;
6use crate::ExpectOpResult;
7use crate::JsonType;
8use chrono::DateTime;
9use chrono::Duration as ChronoDuration;
10use chrono::FixedOffset;
11use chrono::Offset;
12use chrono::Utc;
13use std::time::Duration as StdDuration;
14
15#[expect_op(internal, name = "iso_date_time")]
60#[derive(Debug, Clone, Default, PartialEq)]
61pub struct ExpectIsoDateTime {
62 is_utc_only: bool,
63 maybe_past_duration: Option<StdDuration>,
64 maybe_future_duration: Option<StdDuration>,
65}
66
67impl ExpectIsoDateTime {
68 pub(crate) fn new() -> Self {
69 Self {
70 is_utc_only: true,
71 maybe_past_duration: None,
72 maybe_future_duration: None,
73 }
74 }
75
76 pub fn allow_non_utc(self) -> Self {
83 Self {
84 is_utc_only: false,
85 ..self
86 }
87 }
88
89 pub fn within_past(self, duration: StdDuration) -> Self {
98 Self {
99 maybe_past_duration: Some(duration),
100 ..self
101 }
102 }
103
104 pub fn within_future(self, duration: StdDuration) -> Self {
113 Self {
114 maybe_future_duration: Some(duration),
115 ..self
116 }
117 }
118}
119
120impl ExpectOp for ExpectIsoDateTime {
121 fn on_string(&self, context: &mut Context, received: &str) -> ExpectOpResult<()> {
122 let date_time = DateTime::<FixedOffset>::parse_from_rfc3339(received).map_err(|error| {
123 let error_message = format!("failed to parse string '{received}' as iso date time");
124 ExpectOpError::custom_error(context, self, error_message, error)
125 })?;
126
127 if self.is_utc_only {
128 let is_date_time_utc = date_time.offset().fix().utc_minus_local() != 0;
129 if is_date_time_utc {
130 let error_message = format!(
131 "ISO datetime '{received}' is using a non-UTC timezone, expected UTC only"
132 );
133 return Err(ExpectOpError::custom(context, self, error_message));
134 }
135 }
136
137 match (self.maybe_past_duration, self.maybe_future_duration) {
138 (None, None) => {}
139 (Some(past_duration), None) => {
140 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
141 if is_date_time_outside_past {
142 let duration =
143 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
144 let error_message = format!("ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now");
145 return Err(ExpectOpError::custom(context, self, error_message));
146 }
147
148 let is_date_time_ahead_of_now = date_time > Utc::now();
149 if is_date_time_ahead_of_now {
150 let duration =
151 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
152 let error_message = format!("ISO datetime '{received}' is in the future of now, expected between '{duration}' ago and now");
153 return Err(ExpectOpError::custom(context, self, error_message));
154 }
155 }
156 (None, Some(future_duration)) => {
157 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
158 if is_date_time_outside_future {
159 let duration =
160 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
161 let error_message = format!("ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future");
162 return Err(ExpectOpError::custom(context, self, error_message));
163 }
164
165 let is_date_time_behind_of_now = date_time < Utc::now();
166 if is_date_time_behind_of_now {
167 let duration =
168 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
169 let error_message = format!("ISO datetime '{received}' is in the past of now, expected between now and '{duration}' in the future");
170 return Err(ExpectOpError::custom(context, self, error_message));
171 }
172 }
173 (Some(past_duration), Some(future_duration)) => {
174 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
175 if is_date_time_outside_past {
176 let duration =
177 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
178 let error_message = format!("ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now");
179 return Err(ExpectOpError::custom(context, self, error_message));
180 }
181
182 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
183 if is_date_time_outside_future {
184 let duration =
185 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
186 let error_message = format!("ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future");
187 return Err(ExpectOpError::custom(context, self, error_message));
188 }
189 }
190 }
191
192 Ok(())
193 }
194
195 fn supported_types(&self) -> &'static [JsonType] {
196 &[JsonType::String]
197 }
198}
199
200#[cfg(test)]
201mod test_iso_date_time {
202 use crate::expect;
203 use crate::expect_json_eq;
204 use pretty_assertions::assert_eq;
205 use serde_json::json;
206
207 #[test]
208 fn it_should_parse_iso_datetime_with_utc_timezone() {
209 let left = json!("2024-01-15T13:45:30Z");
210 let right = json!(expect::iso_date_time());
211
212 let output = expect_json_eq(&left, &right);
213 assert!(output.is_ok(), "assertion error: {output:#?}");
214 }
215
216 #[test]
217 fn it_should_fail_to_parse_iso_datetime_with_non_utc_timezone() {
218 let left = json!("2024-01-15T13:45:30+01:00");
219 let right = json!(expect::iso_date_time());
220
221 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
222 assert_eq!(
223 output,
224 r#"Json expect::iso_date_time() error at root:
225 ISO datetime '2024-01-15T13:45:30+01:00' is using a non-UTC timezone, expected UTC only"#
226 );
227 }
228
229 #[test]
230 fn it_should_fail_to_parse_iso_datetime_without_timezone() {
231 let left = json!("2024-01-15T13:45:30");
232 let right = json!(expect::iso_date_time());
233
234 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
235 assert_eq!(
236 output,
237 r#"Json expect::iso_date_time() error at root:
238 failed to parse string '2024-01-15T13:45:30' as iso date time,
239 premature end of input"#
240 );
241 }
242}
243
244#[cfg(test)]
245mod test_utc_only {
246 use crate::expect;
247 use crate::expect_json_eq;
248 use serde_json::json;
249
250 #[test]
251 fn it_should_parse_iso_datetime_with_utc_timezone_when_set() {
252 let left = json!("2024-01-15T13:45:30Z");
253 let right = json!(expect::iso_date_time().allow_non_utc());
254
255 let output = expect_json_eq(&left, &right);
256 assert!(output.is_ok(), "assertion error: {output:#?}");
257 }
258
259 #[test]
260 fn it_should_parse_iso_datetime_with_non_utc_timezone_when_set() {
261 let left = json!("2024-01-15T13:45:30+01:00");
262 let right = json!(expect::iso_date_time().allow_non_utc());
263
264 let output = expect_json_eq(&left, &right);
265 assert!(output.is_ok(), "assertion error: {output:#?}");
266 }
267}
268
269#[cfg(test)]
270mod test_within_past {
271 use super::*;
272 use crate::expect;
273 use crate::expect_json_eq;
274 use pretty_assertions::assert_eq;
275 use serde_json::json;
276
277 #[test]
278 fn it_should_parse_iso_datetime_within_past_set() {
279 let now = Utc::now();
280 let now_str = (now - ChronoDuration::seconds(30)).to_rfc3339();
281 let left = json!(now_str);
282 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
283
284 let output = expect_json_eq(&left, &right);
285 assert!(output.is_ok(), "assertion error: {output:#?}");
286 }
287
288 #[test]
289 fn it_should_not_parse_iso_datetime_within_past_too_far() {
290 let now = Utc::now();
291 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
292 let left = json!(now_str);
293 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
294
295 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
296 assert_eq!(
297 output,
298 format!(
299 r#"Json expect::iso_date_time() error at root:
300 ISO datetime '{now_str}' is too far from the past, expected between '1 minute' ago and now"#
301 )
302 );
303 }
304
305 #[test]
306 fn it_should_not_parse_iso_datetime_ahead_of_now() {
307 let now = Utc::now();
308 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
309 let left = json!(now_str);
310 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
311
312 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
313 assert_eq!(
314 output,
315 format!(
316 r#"Json expect::iso_date_time() error at root:
317 ISO datetime '{now_str}' is in the future of now, expected between '1 minute' ago and now"#
318 )
319 );
320 }
321}
322
323#[cfg(test)]
324mod test_within_future {
325 use super::*;
326 use crate::expect;
327 use crate::expect_json_eq;
328 use pretty_assertions::assert_eq;
329 use serde_json::json;
330
331 #[test]
332 fn it_should_parse_iso_datetime_within_future_set() {
333 let now = Utc::now();
334 let now_str = (now + ChronoDuration::seconds(30)).to_rfc3339();
335 let left = json!(now_str);
336 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
337
338 let output = expect_json_eq(&left, &right);
339 assert!(output.is_ok(), "assertion error: {output:#?}");
340 }
341
342 #[test]
343 fn it_should_not_parse_iso_datetime_within_past_too_far() {
344 let now = Utc::now();
345 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
346 let left = json!(now_str);
347 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
348
349 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
350 assert_eq!(
351 output,
352 format!(
353 r#"Json expect::iso_date_time() error at root:
354 ISO datetime '{now_str}' is too far in the future, expected between now and '1 minute' in the future"#
355 )
356 );
357 }
358
359 #[test]
360 fn it_should_not_parse_iso_datetime_before_now() {
361 let now = Utc::now();
362 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
363 let left = json!(now_str);
364 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
365
366 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
367 assert_eq!(
368 output,
369 format!(
370 r#"Json expect::iso_date_time() error at root:
371 ISO datetime '{now_str}' is in the past of now, expected between now and '1 minute' in the future"#
372 )
373 );
374 }
375}