1use crate::{JsonMatcher, JsonMatcherError};
2use chrono::{DateTime, Duration, FixedOffset, Utc};
3use chrono_tz::Tz;
4use serde_json::Value;
5
6fn parse_datetime_from_string(
7 s: &str,
8 timezone: Option<&str>,
9) -> Result<DateTime<FixedOffset>, String> {
10 let datetime = match DateTime::parse_from_rfc3339(s) {
11 Ok(x) => x,
12 Err(e) => {
13 let parsed = DateTime::parse_from_rfc3339(&(s.to_owned() + "Z")).map_err(|_| {
16 format!("Value cannot be parsed as an RFC 3339 timestamp: {e}")
19 })?;
20 let corrected = match timezone.as_ref() {
22 None => parsed,
23 Some(tz) => match tz.parse::<Tz>() {
24 Ok(tz) => {
25 let with_timezone: DateTime<Tz> =
28 parsed.naive_utc().and_local_timezone(tz).unwrap();
29 with_timezone.fixed_offset()
30 }
31 Err(_) => {
32 parsed
35 }
36 },
37 };
38 corrected
39 }
40 };
41 Ok(datetime)
42}
43
44pub struct DateTimeStringMatcher {
45 lower_bound: Option<DateTime<Utc>>,
46 lower_bound_inclusive: bool,
47 upper_bound: Option<DateTime<Utc>>,
48 upper_bound_inclusive: bool,
49}
50
51impl DateTimeStringMatcher {
52 pub fn recent_utc() -> Self {
53 Self {
54 lower_bound: Some(Utc::now() - Duration::minutes(1)),
55 lower_bound_inclusive: true,
56 upper_bound: Some(Utc::now()),
57 upper_bound_inclusive: true,
58 }
59 }
60}
61
62impl JsonMatcher for DateTimeStringMatcher {
63 fn json_matches(&self, value: &Value) -> Vec<JsonMatcherError> {
64 let Value::String(as_str) = value else {
65 return vec![JsonMatcherError::at_root(
66 "Datetime value needs to be a string",
67 )];
68 };
69 let datetime = match parse_datetime_from_string(as_str, None) {
70 Ok(parsed) => parsed,
71 Err(err) => {
72 return vec![JsonMatcherError::at_root(format!(
73 "Could not parse string as rfc3339 datetime: {}",
74 err
75 ))];
76 }
77 };
78 if datetime.offset().utc_minus_local() != 0 {
79 return vec![JsonMatcherError::at_root("Datetime is not in UTC")];
80 }
81 if let Some(upper_bound) = self.upper_bound {
82 if self.upper_bound_inclusive {
83 if datetime.timestamp() > upper_bound.timestamp() {
84 return vec![JsonMatcherError::at_root("Datetime is after upper bound")];
85 }
86 } else if datetime.timestamp() >= upper_bound.timestamp() {
87 return vec![JsonMatcherError::at_root(
88 "Datetime is after or equal to upper bound",
89 )];
90 }
91 }
92 if let Some(lower_bound) = self.lower_bound {
93 if self.lower_bound_inclusive {
94 if datetime.timestamp() < lower_bound.timestamp() {
95 return vec![JsonMatcherError::at_root(format!(
96 "Datetime is before lower bound of {}",
97 lower_bound.to_rfc3339()
98 ))];
99 }
100 } else if datetime.timestamp() <= lower_bound.timestamp() {
101 return vec![JsonMatcherError::at_root(
102 "Datetime is before or equal to lower bound",
103 )];
104 }
105 }
106 vec![]
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use crate::assert_jm;
113 use serde_json::json;
114
115 use super::*;
116
117 #[test]
118 fn test_date_time_string_matcher() {
119 let lower_bound = DateTime::parse_from_rfc3339("2024-01-05T10:00:00Z")
120 .unwrap()
121 .naive_utc()
122 .and_utc();
123 let upper_bound = DateTime::parse_from_rfc3339("2024-01-05T11:00:00Z")
124 .unwrap()
125 .naive_utc()
126 .and_utc();
127 let matcher = DateTimeStringMatcher {
128 lower_bound: Some(lower_bound),
129 lower_bound_inclusive: true,
130 upper_bound: Some(upper_bound),
131 upper_bound_inclusive: true,
132 };
133 assert_jm!(json!("2024-01-05T10:00:00Z"), matcher);
135 assert_jm!(json!("2024-01-05T10:30:00Z"), matcher);
136 assert_jm!(json!("2024-01-05T11:00:00Z"), matcher);
137 assert_eq!(
139 matcher.json_matches(&json!(2)),
140 vec![JsonMatcherError::at_root(
141 "Datetime value needs to be a string"
142 )]
143 );
144 assert_eq!(
145 matcher.json_matches(&json!("bloop")),
146 vec![JsonMatcherError::at_root(
147 "Could not parse string as rfc3339 datetime: Value cannot be parsed as an RFC 3339 timestamp: input contains invalid characters"
148 )]
149 );
150 assert_eq!(
151 matcher.json_matches(&json!("2024-22-05T10:00:00Z")),
152 vec![JsonMatcherError::at_root(
153 "Could not parse string as rfc3339 datetime: Value cannot be parsed as an RFC 3339 timestamp: input is out of range"
154 )]
155 );
156 assert_eq!(
157 matcher.json_matches(&json!("2024-01-05T09:59:59Z")),
158 vec![JsonMatcherError::at_root(
159 "Datetime is before lower bound of 2024-01-05T10:00:00+00:00"
160 )]
161 );
162 assert_eq!(
163 matcher.json_matches(&json!("2024-01-05T11:00:01Z")),
164 vec![JsonMatcherError::at_root("Datetime is after upper bound")]
165 );
166 assert_eq!(
167 matcher.json_matches(&json!("2024-01-05T11:00:01-08:00")),
168 vec![JsonMatcherError::at_root("Datetime is not in UTC")]
169 );
170 }
171}