reqsign-core 3.0.1

Signing API requests without effort.
Documentation
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

//! Time related utils.

use crate::Error;
use std::fmt;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::str::FromStr;
use std::time::{Duration, SystemTime};

/// An instant in time represented as the number of nanoseconds since the Unix epoch.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp(jiff::Timestamp);

impl FromStr for Timestamp {
    type Err = Error;

    /// Parse a timestamp by the default [`DateTimeParser`].
    ///
    /// All of them are valid time:
    ///
    /// - `2022-03-13T07:20:04Z`
    /// - `2022-03-01T08:12:34+00:00`
    /// - `2022-03-01T08:12:34.00+00:00`
    /// - `2022-07-08T02:14:07+02:00[Europe/Paris]`
    ///
    /// [`DateTimeParser`]: jiff::fmt::temporal::DateTimeParser
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.parse() {
            Ok(t) => Ok(Timestamp(t)),
            Err(err) => Err(
                Error::unexpected(format!("parse '{s}' into timestamp failed")).with_source(err),
            ),
        }
    }
}

impl fmt::Display for Timestamp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Timestamp {
    /// Create the timestamp of now.
    pub fn now() -> Self {
        Self(jiff::Timestamp::now())
    }

    /// Format the timestamp into date: `20220301`
    pub fn format_date(self) -> String {
        self.0.strftime("%Y%m%d").to_string()
    }

    /// Format the timestamp into ISO8601: `20220313T072004Z`
    pub fn format_iso8601(self) -> String {
        self.0.strftime("%Y%m%dT%H%M%SZ").to_string()
    }

    /// Format the timestamp into http date: `Sun, 06 Nov 1994 08:49:37 GMT`
    ///
    /// ## Note
    ///
    /// HTTP date is slightly different from RFC2822.
    ///
    /// - Timezone is fixed to GMT.
    /// - Day must be 2 digit.
    pub fn format_http_date(self) -> String {
        self.0.strftime("%a, %d %b %Y %T GMT").to_string()
    }

    /// Format the timestamp into RFC3339 in Zulu: `2022-03-13T07:20:04Z`
    pub fn format_rfc3339_zulu(self) -> String {
        self.0.strftime("%FT%TZ").to_string()
    }

    /// Returns this timestamp as a number of seconds since the Unix epoch.
    ///
    /// This only returns the number of whole seconds. That is, if there are
    /// any fractional seconds in this timestamp, then they are truncated.
    pub fn as_second(self) -> i64 {
        self.0.as_second()
    }

    /// Returns the fractional second component of this timestamp in units of
    /// nanoseconds.
    ///
    /// It is guaranteed that this will never return a value that is greater
    /// than 1 second (or less than -1 second).
    pub fn subsec_nanosecond(self) -> i32 {
        self.0.subsec_nanosecond()
    }

    /// Convert to `SystemTime`.
    pub fn as_system_time(self) -> SystemTime {
        SystemTime::from(self.0)
    }

    /// Creates a new instant in time from the number of milliseconds elapsed
    /// since the Unix epoch.
    ///
    /// When `millisecond` is negative, it corresponds to an instant in time
    /// before the Unix epoch. A smaller number corresponds to an instant in
    /// time further into the past.
    pub fn from_millisecond(millis: i64) -> crate::Result<Self> {
        match jiff::Timestamp::from_millisecond(millis) {
            Ok(t) => Ok(Timestamp(t)),
            Err(err) => Err(Error::unexpected(format!(
                "convert '{millis}' milliseconds into timestamp failed"
            ))
            .with_source(err)),
        }
    }

    /// Creates a new instant in time from the number of seconds elapsed since
    /// the Unix epoch.
    ///
    /// When `second` is negative, it corresponds to an instant in time before
    /// the Unix epoch. A smaller number corresponds to an instant in time
    /// further into the past.
    pub fn from_second(second: i64) -> crate::Result<Self> {
        match jiff::Timestamp::from_second(second) {
            Ok(t) => Ok(Timestamp(t)),
            Err(err) => Err(Error::unexpected(format!(
                "convert '{second}' seconds into timestamp failed"
            ))
            .with_source(err)),
        }
    }

    /// Parse a timestamp from RFC2822.
    ///
    /// All of them are valid time:
    ///
    /// - `Sat, 13 Jul 2024 15:09:59 -0400`
    /// - `Mon, 15 Aug 2022 16:50:12 GMT`
    pub fn parse_rfc2822(s: &str) -> crate::Result<Timestamp> {
        match jiff::fmt::rfc2822::parse(s) {
            Ok(zoned) => Ok(Timestamp(zoned.timestamp())),
            Err(err) => {
                Err(Error::unexpected(format!("parse '{s}' into rfc2822 failed")).with_source(err))
            }
        }
    }

    /// Parse the string format "2023-10-31 21:59:10.000000".
    pub fn parse_datetime_utc(s: &str) -> crate::Result<Timestamp> {
        let dt = s.parse::<jiff::civil::DateTime>().map_err(|err| {
            Error::unexpected(format!("parse '{s}' into datetime failed")).with_source(err)
        })?;

        let ts = jiff::tz::TimeZone::UTC.to_timestamp(dt).map_err(|err| {
            Error::unexpected(format!("convert '{s}' into timestamp failed")).with_source(err)
        })?;

        Ok(Timestamp(ts))
    }
}

impl Add<Duration> for Timestamp {
    type Output = Timestamp;

    fn add(self, rhs: Duration) -> Timestamp {
        let ts = self
            .0
            .checked_add(rhs)
            .expect("adding unsigned duration to timestamp overflowed");

        Timestamp(ts)
    }
}

impl AddAssign<Duration> for Timestamp {
    fn add_assign(&mut self, rhs: Duration) {
        *self = *self + rhs
    }
}

impl Sub<Duration> for Timestamp {
    type Output = Timestamp;

    fn sub(self, rhs: Duration) -> Timestamp {
        let ts = self
            .0
            .checked_sub(rhs)
            .expect("subtracting unsigned duration from timestamp overflowed");

        Timestamp(ts)
    }
}

impl SubAssign<Duration> for Timestamp {
    fn sub_assign(&mut self, rhs: Duration) {
        *self = *self - rhs
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_time() -> Timestamp {
        Timestamp("2022-03-01T08:12:34Z".parse().unwrap())
    }

    #[test]
    fn test_format_date() {
        let t = test_time();
        assert_eq!("20220301", t.format_date())
    }

    #[test]
    fn test_format_ios8601() {
        let t = test_time();
        assert_eq!("20220301T081234Z", t.format_iso8601())
    }

    #[test]
    fn test_format_http_date() {
        let t = test_time();
        assert_eq!("Tue, 01 Mar 2022 08:12:34 GMT", t.format_http_date())
    }

    #[test]
    fn test_format_rfc3339() {
        let t = test_time();
        assert_eq!("2022-03-01T08:12:34Z", t.format_rfc3339_zulu())
    }

    #[test]
    fn test_parse_rfc3339() {
        let t = test_time();

        for v in [
            "2022-03-01T08:12:34Z",
            "2022-03-01T08:12:34+00:00",
            "2022-03-01T08:12:34.00+00:00",
        ] {
            assert_eq!(t, v.parse().expect("must be valid time"));
        }
    }
}