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: false,
58 maybe_past_duration: None,
59 maybe_future_duration: None,
60 }
61 }
62
63 pub fn utc(self) -> Self {
98 Self {
99 is_utc_only: true,
100 ..self
101 }
102 }
103
104 pub fn within_past(self, duration: StdDuration) -> Self {
142 Self {
143 maybe_past_duration: Some(duration),
144 ..self
145 }
146 }
147
148 pub fn within_future(self, duration: StdDuration) -> Self {
185 Self {
186 maybe_future_duration: Some(duration),
187 ..self
188 }
189 }
190}
191
192impl ExpectOp for ExpectIsoDateTime {
193 fn on_string(&self, context: &mut Context, received: &str) -> ExpectOpResult<()> {
194 let date_time = DateTime::<FixedOffset>::parse_from_rfc3339(received).map_err(|error| {
195 let error_message = format!("failed to parse string '{received}' as iso date time");
196 ExpectOpError::custom_error(self, context, error_message, error)
197 })?;
198
199 if self.is_utc_only {
200 let is_date_time_utc = date_time.offset().fix().utc_minus_local() != 0;
201 if is_date_time_utc {
202 let error_message = format!(
203 "ISO datetime '{received}' is using a non-UTC timezone, expected UTC only"
204 );
205 return Err(ExpectOpError::custom(self, context, error_message));
206 }
207 }
208
209 match (self.maybe_past_duration, self.maybe_future_duration) {
210 (None, None) => {}
211 (Some(past_duration), None) => {
212 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
213 if is_date_time_outside_past {
214 let duration =
215 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
216 let error_message = format!("ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now");
217 return Err(ExpectOpError::custom(self, context, error_message));
218 }
219
220 let is_date_time_ahead_of_now = date_time > Utc::now();
221 if is_date_time_ahead_of_now {
222 let duration =
223 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
224 let error_message = format!("ISO datetime '{received}' is in the future of now, expected between '{duration}' ago and now");
225 return Err(ExpectOpError::custom(self, context, error_message));
226 }
227 }
228 (None, Some(future_duration)) => {
229 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
230 if is_date_time_outside_future {
231 let duration =
232 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
233 let error_message = format!("ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future");
234 return Err(ExpectOpError::custom(self, context, error_message));
235 }
236
237 let is_date_time_behind_of_now = date_time < Utc::now();
238 if is_date_time_behind_of_now {
239 let duration =
240 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
241 let error_message = format!("ISO datetime '{received}' is in the past of now, expected between now and '{duration}' in the future");
242 return Err(ExpectOpError::custom(self, context, error_message));
243 }
244 }
245 (Some(past_duration), Some(future_duration)) => {
246 let is_date_time_outside_past = date_time < Utc::now() - past_duration;
247 if is_date_time_outside_past {
248 let duration =
249 DurationFormatter::new(ChronoDuration::from_std(past_duration).unwrap());
250 let error_message = format!("ISO datetime '{received}' is too far from the past, expected between '{duration}' ago and now");
251 return Err(ExpectOpError::custom(self, context, error_message));
252 }
253
254 let is_date_time_outside_future = date_time > Utc::now() + future_duration;
255 if is_date_time_outside_future {
256 let duration =
257 DurationFormatter::new(ChronoDuration::from_std(future_duration).unwrap());
258 let error_message = format!("ISO datetime '{received}' is too far in the future, expected between now and '{duration}' in the future");
259 return Err(ExpectOpError::custom(self, context, error_message));
260 }
261 }
262 }
263
264 Ok(())
265 }
266
267 fn debug_supported_types(&self) -> &'static [JsonType] {
268 &[JsonType::String]
269 }
270}
271
272#[cfg(test)]
273mod test_iso_date_time {
274 use crate::expect;
275 use crate::expect_json_eq;
276 use pretty_assertions::assert_eq;
277 use serde_json::json;
278
279 #[test]
280 fn it_should_parse_iso_datetime_with_utc_timezone() {
281 let left = json!("2024-01-15T13:45:30Z");
282 let right = json!(expect::iso_date_time());
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_parse_iso_datetime_with_non_utc_timezone_by_default() {
290 let left = json!("2024-01-15T13:45:30+01:00");
291 let right = json!(expect::iso_date_time());
292
293 let output = expect_json_eq(&left, &right);
294 assert!(output.is_ok(), "assertion error: {output:#?}");
295 }
296
297 #[test]
298 fn it_should_fail_to_parse_iso_datetime_without_timezone() {
299 let left = json!("2024-01-15T13:45:30");
300 let right = json!(expect::iso_date_time());
301
302 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
303 assert_eq!(
304 output,
305 r#"Json expect::iso_date_time() error at root:
306 failed to parse string '2024-01-15T13:45:30' as iso date time,
307 premature end of input"#
308 );
309 }
310}
311
312#[cfg(test)]
313mod test_utc {
314 use crate::expect;
315 use crate::expect_json_eq;
316 use pretty_assertions::assert_eq;
317 use serde_json::json;
318
319 #[test]
320 fn it_should_parse_iso_datetime_with_utc_timezone_when_set() {
321 let left = json!("2024-01-15T13:45:30Z");
322 let right = json!(expect::iso_date_time().utc());
323
324 let output = expect_json_eq(&left, &right);
325 assert!(output.is_ok(), "assertion error: {output:#?}");
326 }
327
328 #[test]
329 fn it_should_not_parse_iso_datetime_with_non_utc_timezone_when_set() {
330 let left = json!("2024-01-15T13:45:30+01:00");
331 let right = json!(expect::iso_date_time().utc());
332
333 let output = expect_json_eq(&left, &right).unwrap_err().to_string();
334 assert_eq!(
335 output,
336 "Json expect::iso_date_time() error at root:
337 ISO datetime '2024-01-15T13:45:30+01:00' is using a non-UTC timezone, expected UTC only"
338 );
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}