Skip to main content

slack_morphism/models/common/
datetime.rs

1//! Date and time types used across the Slack models.
2//!
3//! The concrete time crate is hidden behind [`SlackUtcDateTime`] and [`SlackCivilDate`].
4//! By default they are backed by [`jiff`]. Enabling the deprecated `obsolete-chrono`
5//! feature switches them back to the `chrono` types the crate used before.
6//!
7//! This module is the only place in the crate that refers to a time crate directly.
8
9/// An instant in time in UTC, as used by the Slack API.
10///
11/// `jiff::Timestamp` by default, `chrono::DateTime<chrono::Utc>` with the
12/// deprecated `obsolete-chrono` feature.
13#[cfg(not(feature = "obsolete-chrono"))]
14pub type SlackUtcDateTime = jiff::Timestamp;
15
16/// An instant in time in UTC, as used by the Slack API.
17///
18/// `jiff::Timestamp` by default, `chrono::DateTime<chrono::Utc>` with the
19/// deprecated `obsolete-chrono` feature.
20#[cfg(feature = "obsolete-chrono")]
21pub type SlackUtcDateTime = chrono::DateTime<chrono::Utc>;
22
23/// A calendar date without a time zone, as used by the Slack API.
24///
25/// `jiff::civil::Date` by default, `chrono::NaiveDate` with the deprecated
26/// `obsolete-chrono` feature.
27#[cfg(not(feature = "obsolete-chrono"))]
28pub type SlackCivilDate = jiff::civil::Date;
29
30/// A calendar date without a time zone, as used by the Slack API.
31///
32/// `jiff::civil::Date` by default, `chrono::NaiveDate` with the deprecated
33/// `obsolete-chrono` feature.
34#[cfg(feature = "obsolete-chrono")]
35pub type SlackCivilDate = chrono::NaiveDate;
36
37/// Serde adapter for Slack's integer unix-seconds fields.
38///
39/// Used as `#[serde(with = "unix_seconds")]` on a [`SlackUtcDateTime`] field.
40pub(crate) mod unix_seconds {
41    #[cfg(not(feature = "obsolete-chrono"))]
42    pub use jiff::fmt::serde::timestamp::second::required::{deserialize, serialize};
43
44    #[cfg(feature = "obsolete-chrono")]
45    pub use chrono::serde::ts_seconds::{deserialize, serialize};
46}
47
48/// The current instant in UTC.
49pub(crate) fn now() -> SlackUtcDateTime {
50    #[cfg(not(feature = "obsolete-chrono"))]
51    {
52        jiff::Timestamp::now()
53    }
54    #[cfg(feature = "obsolete-chrono")]
55    {
56        chrono::Utc::now()
57    }
58}
59
60/// Builds an instant from unix seconds and an additional microseconds offset.
61///
62/// Returns `None` when the resulting instant is out of the supported range.
63pub(crate) fn from_unix_seconds_micros(secs: i64, micros: u32) -> Option<SlackUtcDateTime> {
64    let nanos = i32::try_from(micros).ok()?.checked_mul(1_000)?;
65
66    #[cfg(not(feature = "obsolete-chrono"))]
67    {
68        jiff::Timestamp::new(secs, nanos).ok()
69    }
70    #[cfg(feature = "obsolete-chrono")]
71    {
72        use chrono::TimeZone;
73        match chrono::Utc.timestamp_opt(secs, u32::try_from(nanos).ok()?) {
74            chrono::LocalResult::None => None,
75            chrono::LocalResult::Single(result) => Some(result),
76            chrono::LocalResult::Ambiguous(first, _) => Some(first),
77        }
78    }
79}
80
81/// The number of whole seconds since the unix epoch.
82// Only the jiff path of `fmt_slack_date` calls this: the chrono path keeps its generic
83// `DateTime<TZ>` signature and uses chrono's own inherent methods. Still exercised by tests
84// in both modes, so it is kept rather than gated out.
85#[cfg_attr(feature = "obsolete-chrono", allow(dead_code))]
86pub(crate) fn unix_seconds(date_time: &SlackUtcDateTime) -> i64 {
87    #[cfg(not(feature = "obsolete-chrono"))]
88    {
89        date_time.as_second()
90    }
91    #[cfg(feature = "obsolete-chrono")]
92    {
93        date_time.timestamp()
94    }
95}
96
97/// Formats an instant as an RFC 2822 date-time in UTC.
98// See the note on `unix_seconds` for why this is allowed to be unused under `obsolete-chrono`.
99#[cfg_attr(feature = "obsolete-chrono", allow(dead_code))]
100pub(crate) fn to_rfc2822(date_time: &SlackUtcDateTime) -> String {
101    #[cfg(not(feature = "obsolete-chrono"))]
102    {
103        jiff::fmt::rfc2822::to_string(&date_time.to_zoned(jiff::tz::TimeZone::UTC))
104            .unwrap_or_else(|_| date_time.to_string())
105    }
106    #[cfg(feature = "obsolete-chrono")]
107    {
108        date_time.to_rfc2822()
109    }
110}
111
112/// Parses a `YYYY-MM-DD` calendar date, rejecting anything else.
113pub(crate) fn parse_civil_date(value: &str) -> Option<SlackCivilDate> {
114    #[cfg(not(feature = "obsolete-chrono"))]
115    {
116        jiff::fmt::strtime::parse("%Y-%m-%d", value)
117            .and_then(|parsed| parsed.to_date())
118            .ok()
119    }
120    #[cfg(feature = "obsolete-chrono")]
121    {
122        SlackCivilDate::parse_from_str(value, "%Y-%m-%d").ok()
123    }
124}
125
126/// The current number of whole seconds since the unix epoch, read from the system clock.
127///
128/// Deliberately built on `std` only: the places that need a coarse wall clock reading
129/// (multipart boundaries, signature freshness) should not pull in a date/time crate.
130#[cfg(feature = "signature-verifier")]
131pub(crate) fn current_unix_seconds() -> i64 {
132    std::time::SystemTime::now()
133        .duration_since(std::time::UNIX_EPOCH)
134        .map(|elapsed| elapsed.as_secs() as i64)
135        .unwrap_or_default()
136}