identity_core/common/
timestamp.rs1use core::convert::TryFrom;
5use core::fmt::Debug;
6use core::fmt::Display;
7use core::fmt::Formatter;
8use core::str::FromStr;
9use std::borrow::Borrow;
10use std::borrow::Cow;
11
12use serde;
13use serde::Deserialize;
14use serde::Serialize;
15use time::format_description::well_known::Rfc3339;
16use time::OffsetDateTime;
17use time::UtcOffset;
18
19use crate::error::Error;
20use crate::error::Result;
21
22#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
24#[repr(transparent)]
25#[serde(try_from = "ProvisionalTimestamp<'_>", into = "String")]
26pub struct Timestamp(OffsetDateTime);
27
28impl Timestamp {
29 pub fn parse(input: &str) -> Result<Self> {
34 let offset_date_time = OffsetDateTime::parse(input, &Rfc3339)
35 .map_err(time::Error::from)
36 .map_err(Error::InvalidTimestamp)?
37 .to_offset(UtcOffset::UTC);
38 Ok(Timestamp(truncate_fractional_seconds(offset_date_time)))
39 }
40
41 #[cfg(all(
46 not(all(target_arch = "wasm32", not(target_os = "wasi"))),
47 not(feature = "custom_time")
48 ))]
49 pub fn now_utc() -> Self {
50 Self(truncate_fractional_seconds(OffsetDateTime::now_utc()))
51 }
52
53 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))]
58 pub fn now_utc() -> Self {
59 let milliseconds_since_unix_epoch: i64 = js_sys::Date::now() as i64;
60 let seconds: i64 = milliseconds_since_unix_epoch / 1000;
61 Self::from_unix(seconds).expect("Timestamp failed to convert system datetime")
63 }
64
65 #[cfg(feature = "custom_time")]
70 pub fn now_utc() -> Self {
71 crate::custom_time::now_utc_custom()
72 }
73
74 pub fn to_rfc3339(&self) -> String {
76 self.0.format(&Rfc3339).expect("Timestamp incompatible with RFC 3339")
79 }
80
81 pub fn to_unix(&self) -> i64 {
83 self.0.unix_timestamp()
84 }
85
86 pub fn from_unix(seconds: i64) -> Result<Self> {
93 let offset_date_time = OffsetDateTime::from_unix_timestamp(seconds)
94 .map_err(time::error::Error::from)
95 .map_err(Error::InvalidTimestamp)?;
96
97 if !(0..10_000).contains(&offset_date_time.year()) {
101 return Err(Error::InvalidTimestamp(time::error::Error::Format(
102 time::error::Format::InvalidComponent("invalid year"),
103 )));
104 }
105 Ok(Self(offset_date_time))
106 }
107
108 pub fn checked_add(self, duration: Duration) -> Option<Self> {
112 self
113 .0
114 .checked_add(duration.0)
115 .and_then(|offset_date_time| Self::from_unix(offset_date_time.unix_timestamp()).ok())
116 }
117
118 pub fn checked_sub(self, duration: Duration) -> Option<Self> {
122 self
123 .0
124 .checked_sub(duration.0)
125 .and_then(|offset_date_time| Self::from_unix(offset_date_time.unix_timestamp()).ok())
126 }
127}
128
129impl Default for Timestamp {
130 fn default() -> Self {
131 Self::now_utc()
132 }
133}
134
135impl Debug for Timestamp {
136 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
137 write!(f, "{:?}", self.to_rfc3339())
138 }
139}
140
141impl Display for Timestamp {
142 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
143 write!(f, "{}", self.to_rfc3339())
144 }
145}
146
147impl From<Timestamp> for String {
148 fn from(timestamp: Timestamp) -> Self {
149 timestamp.to_rfc3339()
150 }
151}
152
153impl TryFrom<&'_ str> for Timestamp {
154 type Error = Error;
155
156 fn try_from(string: &'_ str) -> Result<Self, Self::Error> {
157 Self::parse(string)
158 }
159}
160
161#[derive(Deserialize)]
165struct ProvisionalTimestamp<'a>(#[serde(borrow)] Cow<'a, str>);
166
167impl<'a> TryFrom<ProvisionalTimestamp<'a>> for Timestamp {
168 type Error = Error;
169
170 fn try_from(value: ProvisionalTimestamp<'a>) -> Result<Self, Self::Error> {
171 Timestamp::parse(value.0.borrow())
172 }
173}
174
175impl TryFrom<String> for Timestamp {
176 type Error = Error;
177
178 fn try_from(string: String) -> Result<Self, Self::Error> {
179 Self::parse(&string)
180 }
181}
182
183impl FromStr for Timestamp {
184 type Err = Error;
185
186 fn from_str(string: &str) -> Result<Self, Self::Err> {
187 Self::parse(string)
188 }
189}
190
191fn truncate_fractional_seconds(offset_date_time: OffsetDateTime) -> OffsetDateTime {
193 offset_date_time - time::Duration::nanoseconds(offset_date_time.nanosecond() as i64)
194}
195
196#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
200#[repr(transparent)]
201pub struct Duration(time::Duration);
202
203impl Duration {
208 pub const fn seconds(seconds: u32) -> Self {
210 Self(time::Duration::seconds(seconds as i64))
211 }
212 pub const fn minutes(minutes: u32) -> Self {
214 Self(time::Duration::minutes(minutes as i64))
215 }
216
217 pub const fn days(days: u32) -> Self {
219 Self(time::Duration::days(days as i64))
220 }
221
222 pub const fn hours(hours: u32) -> Self {
224 Self(time::Duration::hours(hours as i64))
225 }
226
227 pub const fn weeks(weeks: u32) -> Self {
229 Self(time::Duration::weeks(weeks as i64))
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use crate::common::Timestamp;
236 use crate::convert::FromJson;
237 use crate::convert::ToJson;
238 use proptest::proptest;
239
240 use super::Duration;
241
242 const FIRST_VALID_UNIX_TIMESTAMP: i64 = -62167219200;
244 const LAST_VALID_UNIX_TIMESTAMP: i64 = 253402300799;
246
247 #[test]
248 fn test_parse_valid() {
249 let original = "2020-01-01T00:00:00Z";
250 let timestamp = Timestamp::parse(original).unwrap();
251
252 assert_eq!(timestamp.to_rfc3339(), original);
253
254 let original = "1980-01-01T12:34:56Z";
255 let timestamp = Timestamp::parse(original).unwrap();
256
257 assert_eq!(timestamp.to_rfc3339(), original);
258 }
259
260 #[test]
261 fn test_parse_valid_truncated() {
262 let original = "1980-01-01T12:34:56.789Z";
263 let expected = "1980-01-01T12:34:56Z";
264 let timestamp = Timestamp::parse(original).unwrap();
265
266 assert_eq!(timestamp.to_rfc3339(), expected);
267 }
268
269 #[test]
270 fn test_parse_valid_offset_normalised() {
271 let original = "1937-01-01T12:00:27.87+00:20";
272 let expected = "1937-01-01T11:40:27Z";
273 let timestamp = Timestamp::parse(original).unwrap();
274
275 assert_eq!(timestamp.to_rfc3339(), expected);
276 }
277
278 #[test]
279 fn test_checked_add() {
280 let timestamp = Timestamp::parse("1980-01-01T12:34:56Z").unwrap();
281 let second_later = timestamp.checked_add(Duration::seconds(1)).unwrap();
282 assert_eq!(second_later.to_rfc3339(), "1980-01-01T12:34:57Z");
283 let minute_later = timestamp.checked_add(Duration::minutes(1)).unwrap();
284 assert_eq!(minute_later.to_rfc3339(), "1980-01-01T12:35:56Z");
285 let hour_later = timestamp.checked_add(Duration::hours(1)).unwrap();
286 assert_eq!(hour_later.to_rfc3339(), "1980-01-01T13:34:56Z");
287 let day_later = timestamp.checked_add(Duration::days(1)).unwrap();
288 assert_eq!(day_later.to_rfc3339(), "1980-01-02T12:34:56Z");
289 let week_later = timestamp.checked_add(Duration::weeks(1)).unwrap();
290 assert_eq!(week_later.to_rfc3339(), "1980-01-08T12:34:56Z");
291
292 assert!(Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP)
294 .unwrap()
295 .checked_add(Duration::seconds(1))
296 .is_none());
297 }
298
299 #[test]
300 fn test_checked_sub() {
301 let timestamp = Timestamp::parse("1980-01-01T12:34:56Z").unwrap();
302 let second_earlier = timestamp.checked_sub(Duration::seconds(1)).unwrap();
303 assert_eq!(second_earlier.to_rfc3339(), "1980-01-01T12:34:55Z");
304 let minute_earlier = timestamp.checked_sub(Duration::minutes(1)).unwrap();
305 assert_eq!(minute_earlier.to_rfc3339(), "1980-01-01T12:33:56Z");
306 let hour_earlier = timestamp.checked_sub(Duration::hours(1)).unwrap();
307 assert_eq!(hour_earlier.to_rfc3339(), "1980-01-01T11:34:56Z");
308 let day_earlier = timestamp.checked_sub(Duration::days(1)).unwrap();
309 assert_eq!(day_earlier.to_rfc3339(), "1979-12-31T12:34:56Z");
310 let week_earlier = timestamp.checked_sub(Duration::weeks(1)).unwrap();
311 assert_eq!(week_earlier.to_rfc3339(), "1979-12-25T12:34:56Z");
312
313 assert!(Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP)
315 .unwrap()
316 .checked_sub(Duration::seconds(1))
317 .is_none());
318 }
319
320 #[test]
321 fn test_from_unix_zero_to_rfc3339() {
322 let unix_epoch = Timestamp::from_unix(0).unwrap();
323 assert_eq!(unix_epoch.to_rfc3339(), "1970-01-01T00:00:00Z");
324 }
325
326 #[test]
327 fn test_from_unix_invalid_edge_cases() {
328 assert!(Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP + 1).is_err());
329 assert!(Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP - 1).is_err());
330 }
331
332 #[test]
333 fn test_from_unix_to_rfc3339_boundaries() {
334 let beginning = Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP).unwrap();
335 let end = Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP).unwrap();
336 assert_eq!(beginning.to_rfc3339(), "0000-01-01T00:00:00Z");
337 assert_eq!(end.to_rfc3339(), "9999-12-31T23:59:59Z");
338 }
339
340 proptest! {
341 #[test]
342 fn test_from_unix_to_rfc3339_valid_no_panic(seconds in FIRST_VALID_UNIX_TIMESTAMP..=LAST_VALID_UNIX_TIMESTAMP) {
343 let timestamp = Timestamp::from_unix(seconds).unwrap();
344 let expected_length = "dddd-dd-ddTdd:dd:ddZ".len();
345 assert_eq!(timestamp.to_rfc3339().len(), expected_length);
346 }
347 }
348
349 #[test]
350 #[should_panic = "InvalidTimestamp"]
351 fn test_parse_empty() {
352 Timestamp::parse("").unwrap();
353 }
354
355 #[test]
356 #[should_panic = "InvalidTimestamp"]
357 fn test_parse_invalid_date() {
358 Timestamp::parse("foo bar").unwrap();
359 }
360
361 #[test]
362 #[should_panic = "InvalidTimestamp"]
363 fn test_parse_invalid_fmt() {
364 Timestamp::parse("2020/01/01 03:30:16").unwrap();
365 }
366
367 #[test]
368 fn test_json_vec_roundtrip() {
369 let time1: Timestamp = Timestamp::now_utc();
370 let json: Vec<u8> = time1.to_json_vec().unwrap();
371 let time2: Timestamp = Timestamp::from_json_slice(&json).unwrap();
372
373 assert_eq!(time1, time2);
374 }
375
376 #[test]
377 fn test_json_value_roundtrip() {
378 let time1: Timestamp = Timestamp::now_utc();
379 let json: serde_json::Value = time1.to_json_value().unwrap();
380 let time2: Timestamp = Timestamp::from_json_value(json).unwrap();
381
382 assert_eq!(time1, time2);
383 }
384}