proto-types 0.1.0

Rust types generated from the google.protobuf and buf.validate protobuf packages, plus extra helpers for implementing validation with the protocheck crate.
Documentation
#[cfg(all(feature = "serde", feature = "chrono"))]
mod serde;

#[cfg(feature = "chrono")]
mod timestamp_conversions;
mod timestamp_impls;
mod timestamp_operations;

// From (prost-types)[https://github.com/tokio-rs/prost/blob/master/prost-types/src/timestamp.rs]
use super::*;
use crate::Timestamp;

impl Timestamp {
  /// Normalizes the timestamp to a canonical format.
  ///
  /// Based on [`google::protobuf::util::CreateNormalized`][1].
  ///
  /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
  pub fn normalize(&mut self) {
    // Make sure nanos is in the range.
    if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
      if let Some(seconds) = self
        .seconds
        .checked_add((self.nanos / NANOS_PER_SECOND) as i64)
      {
        self.seconds = seconds;

        self.nanos %= NANOS_PER_SECOND;
      } else if self.nanos < 0 {
        // Negative overflow! Set to the earliest normal value.

        self.seconds = i64::MIN;

        self.nanos = 0;
      } else {
        // Positive overflow! Set to the latest normal value.

        self.seconds = i64::MAX;

        self.nanos = 999_999_999;
      }
    }

    // For Timestamp nanos should be in the range [0, 999999999].

    if self.nanos < 0 {
      if let Some(seconds) = self.seconds.checked_sub(1) {
        self.seconds = seconds;

        self.nanos += NANOS_PER_SECOND;
      } else {
        // Negative overflow! Set to the earliest normal value.

        debug_assert_eq!(self.seconds, i64::MIN);

        self.nanos = 0;
      }
    }

    // TODO: should this be checked?

    // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799,

    //               "invalid timestamp: {:?}", self);
  }

  /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be
  /// normalized.
  ///
  /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1].
  ///
  /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
  pub fn try_normalize(mut self) -> Result<Timestamp, Timestamp> {
    let before = self;

    self.normalize();

    // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp

    // normalization overflowed.

    if (self.seconds == i64::MAX || self.seconds == i64::MIN) && self.seconds != before.seconds {
      Err(before)
    } else {
      Ok(self)
    }
  }

  /// Return a normalized copy of the timestamp to a canonical format.
  ///
  /// Based on [`google::protobuf::util::CreateNormalized`][1].
  ///
  /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
  pub fn normalized(&self) -> Self {
    let mut result = *self;

    result.normalize();

    result
  }

  /// Creates a new `Timestamp` at the start of the provided UTC date.
  pub fn date(year: i64, month: u8, day: u8) -> Result<Timestamp, TimestampError> {
    Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0)
  }

  /// Creates a new `Timestamp` instance with the provided UTC date and time.
  pub fn date_time(
    year: i64,

    month: u8,

    day: u8,

    hour: u8,

    minute: u8,

    second: u8,
  ) -> Result<Timestamp, TimestampError> {
    Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0)
  }

  /// Creates a new `Timestamp` instance with the provided UTC date and time.
  pub fn date_time_nanos(
    year: i64,

    month: u8,

    day: u8,

    hour: u8,

    minute: u8,

    second: u8,

    nanos: u32,
  ) -> Result<Timestamp, TimestampError> {
    let date_time = datetime::DateTime {
      year,

      month,

      day,

      hour,

      minute,

      second,

      nanos,
    };

    Timestamp::try_from(date_time)
  }
}

impl Name for Timestamp {
  const PACKAGE: &'static str = PACKAGE_PREFIX;

  const NAME: &'static str = "Timestamp";

  fn type_url() -> String {
    type_url_for::<Self>()
  }
}

impl From<std::time::SystemTime> for Timestamp {
  fn from(system_time: std::time::SystemTime) -> Timestamp {
    let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) {
      Ok(duration) => {
        let seconds = i64::try_from(duration.as_secs()).unwrap();

        (seconds, duration.subsec_nanos() as i32)
      }

      Err(error) => {
        let duration = error.duration();

        let seconds = i64::try_from(duration.as_secs()).unwrap();

        let nanos = duration.subsec_nanos() as i32;

        if nanos == 0 {
          (-seconds, 0)
        } else {
          (-seconds - 1, 1_000_000_000 - nanos)
        }
      }
    };

    Timestamp { seconds, nanos }
  }
}

/// A timestamp handling error.
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum TimestampError {
  /// Indicates that a [`Timestamp`] could not be converted to
  /// [`SystemTime`][std::time::SystemTime] because it is out of range.
  ///
  /// The range of times that can be represented by `SystemTime` depends on the platform. All
  /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms,
  /// such as Windows and 32-bit Linux, may not be able to represent the full range of
  /// `Timestamp`s.
  OutOfSystemRange(Timestamp),
  /// An error indicating failure to parse a timestamp in RFC-3339 format.
  ParseFailure,
  /// Indicates an error when constructing a timestamp due to invalid date or time data.
  InvalidDateTime,
}

impl fmt::Display for TimestampError {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    match self {
      TimestampError::OutOfSystemRange(timestamp) => {
        write!(
          f,
          "{} is not representable as a `SystemTime` because it is out of range",
          timestamp
        )
      }

      TimestampError::ParseFailure => {
        write!(f, "failed to parse RFC-3339 formatted timestamp")
      }

      TimestampError::InvalidDateTime => {
        write!(f, "invalid date or time")
      }
    }
  }
}

impl std::error::Error for TimestampError {}

impl TryFrom<Timestamp> for std::time::SystemTime {
  type Error = TimestampError;

  fn try_from(mut timestamp: Timestamp) -> Result<std::time::SystemTime, Self::Error> {
    let orig_timestamp = timestamp;

    timestamp.normalize();

    let system_time = if timestamp.seconds >= 0 {
      std::time::UNIX_EPOCH.checked_add(time::Duration::from_secs(timestamp.seconds as u64))
    } else {
      std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs(
        timestamp
          .seconds
          .checked_neg()
          .ok_or(TimestampError::OutOfSystemRange(timestamp))? as u64,
      ))
    };

    let system_time = system_time.and_then(|system_time| {
      system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64))
    });

    system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp))
  }
}

impl FromStr for Timestamp {
  type Err = TimestampError;

  fn from_str(s: &str) -> Result<Timestamp, TimestampError> {
    datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure)
  }
}

impl fmt::Display for Timestamp {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    datetime::DateTime::from(*self).fmt(f)
  }
}