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")]
48#[derive(Debug, Clone, Default, PartialEq)]
49pub struct ExpectIsoDateTime {
50 is_utc_only: bool,
51 maybe_past_duration: Option<StdDuration>,
52 maybe_future_duration: Option<StdDuration>,
53}
54
55impl ExpectIsoDateTime {
56 pub(crate) fn new() -> Self {
57 Self {
58 is_utc_only: true,
59 maybe_past_duration: None,
60 maybe_future_duration: None,
61 }
62 }
63
64 pub fn allow_non_utc(self) -> Self {
101 Self {
102 is_utc_only: false,
103 ..self
104 }
105 }
106
107 pub fn within_past(self, duration: StdDuration) -> Self {
146 Self {
147 maybe_past_duration: Some(duration),
148 ..self
149 }
150 }
151
152 pub fn within_future(self, duration: StdDuration) -> Self {
190 Self {
191 maybe_future_duration: Some(duration),
192 ..self
193 }
194 }
195}
196
197impl ExpectOp for ExpectIsoDateTime {
198 fn on_string(&self, context: &mut Context, received: &str) -> ExpectOpResult<()> {
199 let date_time = DateTime::<FixedOffset>::parse_from_rfc3339(received).map_err(|error| {
200 let error_message = format!("failed to parse string '{received}' as iso date time");
201 ExpectOpError::custom_error(context, self, error_message, error)
202 })?;
203
204 if self.is_utc_only {
205 let is_date_time_utc = date_time.offset().fix().utc_minus_local() != 0;
206 if is_date_time_utc {
207 let error_message = format!(
208 "ISO datetime '{received}' is using a non-UTC timezone, expected UTC only"
209 );
210 return Err(ExpectOpError::custom(context, self, error_message));
211 }
212 }
213
214 match (self.maybe_past_duration, self.maybe_future_duration) {
215 (None, None) => {}
216 (Some(past_duration), None) => {
217 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
218 if is_date_time_outside_past {
219 let duration =
220 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
221 let error_message = format!("ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now");
222 return Err(ExpectOpError::custom(context, self, error_message));
223 }
224
225 let is_date_time_ahead_of_now = date_time > Utc::now();
226 if is_date_time_ahead_of_now {
227 let duration =
228 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
229 let error_message = format!("ISO datetime '{received}' is in the future of now, expected between '{duration}' ago and now");
230 return Err(ExpectOpError::custom(context, self, error_message));
231 }
232 }
233 (None, Some(future_duration)) => {
234 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
235 if is_date_time_outside_future {
236 let duration =
237 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
238 let error_message = format!("ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future");
239 return Err(ExpectOpError::custom(context, self, error_message));
240 }
241
242 let is_date_time_behind_of_now = date_time < Utc::now();
243 if is_date_time_behind_of_now {
244 let duration =
245 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
246 let error_message = format!("ISO datetime '{received}' is in the past of now, expected between now and '{duration}' in the future");
247 return Err(ExpectOpError::custom(context, self, error_message));
248 }
249 }
250 (Some(past_duration), Some(future_duration)) => {
251 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
252 if is_date_time_outside_past {
253 let duration =
254 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
255 let error_message = format!("ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now");
256 return Err(ExpectOpError::custom(context, self, error_message));
257 }
258
259 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
260 if is_date_time_outside_future {
261 let duration =
262 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
263 let error_message = format!("ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future");
264 return Err(ExpectOpError::custom(context, self, error_message));
265 }
266 }
267 }
268
269 Ok(())
270 }
271
272 fn supported_types(&self) -> &'static [JsonType] {
273 &[JsonType::String]
274 }
275}
276
277#[cfg(test)]
278mod test_iso_date_time {
279 use crate::expect;
280 use crate::expect_json_eq;
281 use pretty_assertions::assert_eq;
282 use serde_json::json;
283
284 #[test]
285 fn it_should_parse_iso_datetime_with_utc_timezone() {
286 let left = json!("2024-01-15T13:45:30Z");
287 let right = json!(expect::iso_date_time());
288
289 let output = expect_json_eq(&left, &right);
290 assert!(output.is_ok(), "assertion error: {output:#?}");
291 }
292
293 #[test]
294 fn it_should_fail_to_parse_iso_datetime_with_non_utc_timezone() {
295 let left = json!("2024-01-15T13:45:30+01:00");
296 let right = json!(expect::iso_date_time());
297
298 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
299 assert_eq!(
300 output,
301 r#"Json expect::iso_date_time() error at root:
302 ISO datetime '2024-01-15T13:45:30+01:00' is using a non-UTC timezone, expected UTC only"#
303 );
304 }
305
306 #[test]
307 fn it_should_fail_to_parse_iso_datetime_without_timezone() {
308 let left = json!("2024-01-15T13:45:30");
309 let right = json!(expect::iso_date_time());
310
311 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
312 assert_eq!(
313 output,
314 r#"Json expect::iso_date_time() error at root:
315 failed to parse string '2024-01-15T13:45:30' as iso date time,
316 premature end of input"#
317 );
318 }
319}
320
321#[cfg(test)]
322mod test_utc_only {
323 use crate::expect;
324 use crate::expect_json_eq;
325 use serde_json::json;
326
327 #[test]
328 fn it_should_parse_iso_datetime_with_utc_timezone_when_set() {
329 let left = json!("2024-01-15T13:45:30Z");
330 let right = json!(expect::iso_date_time().allow_non_utc());
331
332 let output = expect_json_eq(&left, &right);
333 assert!(output.is_ok(), "assertion error: {output:#?}");
334 }
335
336 #[test]
337 fn it_should_parse_iso_datetime_with_non_utc_timezone_when_set() {
338 let left = json!("2024-01-15T13:45:30+01:00");
339 let right = json!(expect::iso_date_time().allow_non_utc());
340
341 let output = expect_json_eq(&left, &right);
342 assert!(output.is_ok(), "assertion error: {output:#?}");
343 }
344}
345
346#[cfg(test)]
347mod test_within_past {
348 use super::*;
349 use crate::expect;
350 use crate::expect_json_eq;
351 use pretty_assertions::assert_eq;
352 use serde_json::json;
353
354 #[test]
355 fn it_should_parse_iso_datetime_within_past_set() {
356 let now = Utc::now();
357 let now_str = (now - ChronoDuration::seconds(30)).to_rfc3339();
358 let left = json!(now_str);
359 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
360
361 let output = expect_json_eq(&left, &right);
362 assert!(output.is_ok(), "assertion error: {output:#?}");
363 }
364
365 #[test]
366 fn it_should_not_parse_iso_datetime_within_past_too_far() {
367 let now = Utc::now();
368 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
369 let left = json!(now_str);
370 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
371
372 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
373 assert_eq!(
374 output,
375 format!(
376 r#"Json expect::iso_date_time() error at root:
377 ISO datetime '{now_str}' is too far from the past, expected between '1 minute' ago and now"#
378 )
379 );
380 }
381
382 #[test]
383 fn it_should_not_parse_iso_datetime_ahead_of_now() {
384 let now = Utc::now();
385 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
386 let left = json!(now_str);
387 let right = json!(expect::iso_date_time().within_past(StdDuration::from_secs(60)));
388
389 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
390 assert_eq!(
391 output,
392 format!(
393 r#"Json expect::iso_date_time() error at root:
394 ISO datetime '{now_str}' is in the future of now, expected between '1 minute' ago and now"#
395 )
396 );
397 }
398}
399
400#[cfg(test)]
401mod test_within_future {
402 use super::*;
403 use crate::expect;
404 use crate::expect_json_eq;
405 use pretty_assertions::assert_eq;
406 use serde_json::json;
407
408 #[test]
409 fn it_should_parse_iso_datetime_within_future_set() {
410 let now = Utc::now();
411 let now_str = (now + ChronoDuration::seconds(30)).to_rfc3339();
412 let left = json!(now_str);
413 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
414
415 let output = expect_json_eq(&left, &right);
416 assert!(output.is_ok(), "assertion error: {output:#?}");
417 }
418
419 #[test]
420 fn it_should_not_parse_iso_datetime_within_past_too_far() {
421 let now = Utc::now();
422 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
423 let left = json!(now_str);
424 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
425
426 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
427 assert_eq!(
428 output,
429 format!(
430 r#"Json expect::iso_date_time() error at root:
431 ISO datetime '{now_str}' is too far in the future, expected between now and '1 minute' in the future"#
432 )
433 );
434 }
435
436 #[test]
437 fn it_should_not_parse_iso_datetime_before_now() {
438 let now = Utc::now();
439 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
440 let left = json!(now_str);
441 let right = json!(expect::iso_date_time().within_future(StdDuration::from_secs(60)));
442
443 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
444 assert_eq!(
445 output,
446 format!(
447 r#"Json expect::iso_date_time() error at root:
448 ISO datetime '{now_str}' is in the past of now, expected between now and '1 minute' in the future"#
449 )
450 );
451 }
452
453 #[test]
454 fn it_should_pass_if_date_within_past_and_future() {
455 let now = Utc::now();
456 let now_str = now.to_rfc3339();
457 let left = json!(now_str);
458 let right = json!(expect::iso_date_time()
459 .within_past(StdDuration::from_secs(60))
460 .within_future(StdDuration::from_secs(60)));
461
462 let output = expect_json_eq(&left, &right);
463 assert!(output.is_ok(), "assertion error: {output:#?}");
464 }
465
466 #[test]
467 fn it_should_fail_if_date_behind_past_and_future() {
468 let now = Utc::now();
469 let now_str = (now - ChronoDuration::seconds(90)).to_rfc3339();
470 let left = json!(now_str);
471 let right = json!(expect::iso_date_time()
472 .within_past(StdDuration::from_secs(60))
473 .within_future(StdDuration::from_secs(60)));
474
475 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
476 assert_eq!(
477 output,
478 format!(
479 r#"Json expect::iso_date_time() error at root:
480 ISO datetime '{now_str}' is too far from the past, expected between '1 minute' ago and now"#
481 )
482 );
483 }
484
485 #[test]
486 fn it_should_fail_if_date_ahead_of_past_and_future() {
487 let now = Utc::now();
488 let now_str = (now + ChronoDuration::seconds(90)).to_rfc3339();
489 let left = json!(now_str);
490 let right = json!(expect::iso_date_time()
491 .within_past(StdDuration::from_secs(60))
492 .within_future(StdDuration::from_secs(60)));
493
494 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
495 assert_eq!(
496 output,
497 format!(
498 r#"Json expect::iso_date_time() error at root:
499 ISO datetime '{now_str}' is too far in the future, expected between now and '1 minute' in the future"#
500 )
501 );
502 }
503}