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