identity_core/common/
timestamp.rsuse core::convert::TryFrom;
use core::fmt::Debug;
use core::fmt::Display;
use core::fmt::Formatter;
use core::str::FromStr;
use std::borrow::Borrow;
use std::borrow::Cow;
use serde;
use serde::Deserialize;
use serde::Serialize;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use time::UtcOffset;
use crate::error::Error;
use crate::error::Result;
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[repr(transparent)]
#[serde(try_from = "ProvisionalTimestamp<'_>", into = "String")]
pub struct Timestamp(OffsetDateTime);
impl Timestamp {
pub fn parse(input: &str) -> Result<Self> {
let offset_date_time = OffsetDateTime::parse(input, &Rfc3339)
.map_err(time::Error::from)
.map_err(Error::InvalidTimestamp)?
.to_offset(UtcOffset::UTC);
Ok(Timestamp(truncate_fractional_seconds(offset_date_time)))
}
#[cfg(all(
not(all(target_arch = "wasm32", not(target_os = "wasi"))),
not(feature = "custom_time")
))]
pub fn now_utc() -> Self {
Self(truncate_fractional_seconds(OffsetDateTime::now_utc()))
}
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))]
pub fn now_utc() -> Self {
let milliseconds_since_unix_epoch: i64 = js_sys::Date::now() as i64;
let seconds: i64 = milliseconds_since_unix_epoch / 1000;
Self::from_unix(seconds).expect("Timestamp failed to convert system datetime")
}
#[cfg(feature = "custom_time")]
pub fn now_utc() -> Self {
crate::custom_time::now_utc_custom()
}
pub fn to_rfc3339(&self) -> String {
self.0.format(&Rfc3339).expect("Timestamp incompatible with RFC 3339")
}
pub fn to_unix(&self) -> i64 {
self.0.unix_timestamp()
}
pub fn from_unix(seconds: i64) -> Result<Self> {
let offset_date_time = OffsetDateTime::from_unix_timestamp(seconds)
.map_err(time::error::Error::from)
.map_err(Error::InvalidTimestamp)?;
if !(0..10_000).contains(&offset_date_time.year()) {
return Err(Error::InvalidTimestamp(time::error::Error::Format(
time::error::Format::InvalidComponent("invalid year"),
)));
}
Ok(Self(offset_date_time))
}
pub fn checked_add(self, duration: Duration) -> Option<Self> {
self
.0
.checked_add(duration.0)
.and_then(|offset_date_time| Self::from_unix(offset_date_time.unix_timestamp()).ok())
}
pub fn checked_sub(self, duration: Duration) -> Option<Self> {
self
.0
.checked_sub(duration.0)
.and_then(|offset_date_time| Self::from_unix(offset_date_time.unix_timestamp()).ok())
}
}
impl Default for Timestamp {
fn default() -> Self {
Self::now_utc()
}
}
impl Debug for Timestamp {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}", self.to_rfc3339())
}
}
impl Display for Timestamp {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.to_rfc3339())
}
}
impl From<Timestamp> for String {
fn from(timestamp: Timestamp) -> Self {
timestamp.to_rfc3339()
}
}
impl TryFrom<&'_ str> for Timestamp {
type Error = Error;
fn try_from(string: &'_ str) -> Result<Self, Self::Error> {
Self::parse(string)
}
}
#[derive(Deserialize)]
struct ProvisionalTimestamp<'a>(#[serde(borrow)] Cow<'a, str>);
impl<'a> TryFrom<ProvisionalTimestamp<'a>> for Timestamp {
type Error = Error;
fn try_from(value: ProvisionalTimestamp<'a>) -> Result<Self, Self::Error> {
Timestamp::parse(value.0.borrow())
}
}
impl TryFrom<String> for Timestamp {
type Error = Error;
fn try_from(string: String) -> Result<Self, Self::Error> {
Self::parse(&string)
}
}
impl FromStr for Timestamp {
type Err = Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
Self::parse(string)
}
}
fn truncate_fractional_seconds(offset_date_time: OffsetDateTime) -> OffsetDateTime {
offset_date_time - time::Duration::nanoseconds(offset_date_time.nanosecond() as i64)
}
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[repr(transparent)]
pub struct Duration(time::Duration);
impl Duration {
pub const fn seconds(seconds: u32) -> Self {
Self(time::Duration::seconds(seconds as i64))
}
pub const fn minutes(minutes: u32) -> Self {
Self(time::Duration::minutes(minutes as i64))
}
pub const fn days(days: u32) -> Self {
Self(time::Duration::days(days as i64))
}
pub const fn hours(hours: u32) -> Self {
Self(time::Duration::hours(hours as i64))
}
pub const fn weeks(weeks: u32) -> Self {
Self(time::Duration::weeks(weeks as i64))
}
}
#[cfg(test)]
mod tests {
use crate::common::Timestamp;
use crate::convert::FromJson;
use crate::convert::ToJson;
use proptest::proptest;
use super::Duration;
const FIRST_VALID_UNIX_TIMESTAMP: i64 = -62167219200;
const LAST_VALID_UNIX_TIMESTAMP: i64 = 253402300799;
#[test]
fn test_parse_valid() {
let original = "2020-01-01T00:00:00Z";
let timestamp = Timestamp::parse(original).unwrap();
assert_eq!(timestamp.to_rfc3339(), original);
let original = "1980-01-01T12:34:56Z";
let timestamp = Timestamp::parse(original).unwrap();
assert_eq!(timestamp.to_rfc3339(), original);
}
#[test]
fn test_parse_valid_truncated() {
let original = "1980-01-01T12:34:56.789Z";
let expected = "1980-01-01T12:34:56Z";
let timestamp = Timestamp::parse(original).unwrap();
assert_eq!(timestamp.to_rfc3339(), expected);
}
#[test]
fn test_parse_valid_offset_normalised() {
let original = "1937-01-01T12:00:27.87+00:20";
let expected = "1937-01-01T11:40:27Z";
let timestamp = Timestamp::parse(original).unwrap();
assert_eq!(timestamp.to_rfc3339(), expected);
}
#[test]
fn test_checked_add() {
let timestamp = Timestamp::parse("1980-01-01T12:34:56Z").unwrap();
let second_later = timestamp.checked_add(Duration::seconds(1)).unwrap();
assert_eq!(second_later.to_rfc3339(), "1980-01-01T12:34:57Z");
let minute_later = timestamp.checked_add(Duration::minutes(1)).unwrap();
assert_eq!(minute_later.to_rfc3339(), "1980-01-01T12:35:56Z");
let hour_later = timestamp.checked_add(Duration::hours(1)).unwrap();
assert_eq!(hour_later.to_rfc3339(), "1980-01-01T13:34:56Z");
let day_later = timestamp.checked_add(Duration::days(1)).unwrap();
assert_eq!(day_later.to_rfc3339(), "1980-01-02T12:34:56Z");
let week_later = timestamp.checked_add(Duration::weeks(1)).unwrap();
assert_eq!(week_later.to_rfc3339(), "1980-01-08T12:34:56Z");
assert!(Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP)
.unwrap()
.checked_add(Duration::seconds(1))
.is_none());
}
#[test]
fn test_checked_sub() {
let timestamp = Timestamp::parse("1980-01-01T12:34:56Z").unwrap();
let second_earlier = timestamp.checked_sub(Duration::seconds(1)).unwrap();
assert_eq!(second_earlier.to_rfc3339(), "1980-01-01T12:34:55Z");
let minute_earlier = timestamp.checked_sub(Duration::minutes(1)).unwrap();
assert_eq!(minute_earlier.to_rfc3339(), "1980-01-01T12:33:56Z");
let hour_earlier = timestamp.checked_sub(Duration::hours(1)).unwrap();
assert_eq!(hour_earlier.to_rfc3339(), "1980-01-01T11:34:56Z");
let day_earlier = timestamp.checked_sub(Duration::days(1)).unwrap();
assert_eq!(day_earlier.to_rfc3339(), "1979-12-31T12:34:56Z");
let week_earlier = timestamp.checked_sub(Duration::weeks(1)).unwrap();
assert_eq!(week_earlier.to_rfc3339(), "1979-12-25T12:34:56Z");
assert!(Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP)
.unwrap()
.checked_sub(Duration::seconds(1))
.is_none());
}
#[test]
fn test_from_unix_zero_to_rfc3339() {
let unix_epoch = Timestamp::from_unix(0).unwrap();
assert_eq!(unix_epoch.to_rfc3339(), "1970-01-01T00:00:00Z");
}
#[test]
fn test_from_unix_invalid_edge_cases() {
assert!(Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP + 1).is_err());
assert!(Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP - 1).is_err());
}
#[test]
fn test_from_unix_to_rfc3339_boundaries() {
let beginning = Timestamp::from_unix(FIRST_VALID_UNIX_TIMESTAMP).unwrap();
let end = Timestamp::from_unix(LAST_VALID_UNIX_TIMESTAMP).unwrap();
assert_eq!(beginning.to_rfc3339(), "0000-01-01T00:00:00Z");
assert_eq!(end.to_rfc3339(), "9999-12-31T23:59:59Z");
}
proptest! {
#[test]
fn test_from_unix_to_rfc3339_valid_no_panic(seconds in FIRST_VALID_UNIX_TIMESTAMP..=LAST_VALID_UNIX_TIMESTAMP) {
let timestamp = Timestamp::from_unix(seconds).unwrap();
let expected_length = "dddd-dd-ddTdd:dd:ddZ".len();
assert_eq!(timestamp.to_rfc3339().len(), expected_length);
}
}
#[test]
#[should_panic = "InvalidTimestamp"]
fn test_parse_empty() {
Timestamp::parse("").unwrap();
}
#[test]
#[should_panic = "InvalidTimestamp"]
fn test_parse_invalid_date() {
Timestamp::parse("foo bar").unwrap();
}
#[test]
#[should_panic = "InvalidTimestamp"]
fn test_parse_invalid_fmt() {
Timestamp::parse("2020/01/01 03:30:16").unwrap();
}
#[test]
fn test_json_vec_roundtrip() {
let time1: Timestamp = Timestamp::now_utc();
let json: Vec<u8> = time1.to_json_vec().unwrap();
let time2: Timestamp = Timestamp::from_json_slice(&json).unwrap();
assert_eq!(time1, time2);
}
#[test]
fn test_json_value_roundtrip() {
let time1: Timestamp = Timestamp::now_utc();
let json: serde_json::Value = time1.to_json_value().unwrap();
let time2: Timestamp = Timestamp::from_json_value(json).unwrap();
assert_eq!(time1, time2);
}
}