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