From d1b3a9ad9efc2183c9d16105d094d125fff9d533 Mon Sep 17 00:00:00 2001 From: Quat3rnion Date: Sun, 2 Jun 2024 18:09:43 -0400 Subject: [PATCH] Convert timestamp fields to DateTime's --- src/types/entities/channel.rs | 12 +- src/types/entities/guild.rs | 5 +- src/types/entities/guild_member.rs | 8 +- src/types/entities/message.rs | 9 +- src/types/schema/auth.rs | 5 +- src/types/utils/mod.rs | 2 + src/types/utils/serde.rs | 263 +++++++++++++++++++++++++++++ 7 files changed, 292 insertions(+), 12 deletions(-) create mode 100644 src/types/utils/serde.rs diff --git a/src/types/entities/channel.rs b/src/types/entities/channel.rs index fd0c077..dc1e6ad 100644 --- a/src/types/entities/channel.rs +++ b/src/types/entities/channel.rs @@ -11,7 +11,7 @@ use std::fmt::Debug; use crate::types::Shared; use crate::types::{ entities::{GuildMember, User}, - utils::Snowflake, + utils::{Snowflake, serde::*}, }; #[cfg(feature = "client")] @@ -60,6 +60,7 @@ pub struct Channel { pub icon: Option, pub id: Snowflake, pub last_message_id: Option, + #[serde(with = "ts_seconds_option_str")] pub last_pin_timestamp: Option>, pub managed: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] @@ -160,10 +161,12 @@ pub struct PermissionOverwrite { pub struct ThreadMetadata { pub archived: bool, pub auto_archive_duration: i32, - pub archive_timestamp: String, + #[serde(with = "ts_seconds_str")] + pub archive_timestamp: DateTime, pub locked: bool, pub invitable: Option, - pub create_timestamp: Option, + #[serde(with = "ts_seconds_option_str")] + pub create_timestamp: Option>, } #[derive(Default, Debug, Deserialize, Serialize, Clone)] @@ -172,7 +175,8 @@ pub struct ThreadMetadata { pub struct ThreadMember { pub id: Option, pub user_id: Option, - pub join_timestamp: Option, + #[serde(with = "ts_seconds_option_str")] + pub join_timestamp: Option>, pub flags: Option, pub member: Option>, } diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs index 2b7ae44..b259056 100644 --- a/src/types/entities/guild.rs +++ b/src/types/entities/guild.rs @@ -14,7 +14,7 @@ use crate::types::types::guild_configuration::GuildFeaturesList; use crate::types::{ entities::{Channel, Emoji, RoleObject, Sticker, User, VoiceState, Webhook}, interfaces::WelcomeScreenObject, - utils::Snowflake, + utils::{Snowflake, serde::*}, }; use super::PublicUser; @@ -67,7 +67,8 @@ pub struct Guild { #[cfg_attr(feature = "sqlx", sqlx(skip))] pub invites: Option>, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub joined_at: Option, + #[serde(with = "ts_seconds_option_str")] + pub joined_at: Option>, pub large: Option, pub max_members: Option, pub max_presences: Option, diff --git a/src/types/entities/guild_member.rs b/src/types/entities/guild_member.rs index 5cd5ad1..2a3c184 100644 --- a/src/types/entities/guild_member.rs +++ b/src/types/entities/guild_member.rs @@ -2,10 +2,12 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::Shared; use crate::types::{entities::PublicUser, Snowflake}; +use crate::types::utils::serde::*; #[derive(Debug, Deserialize, Default, Serialize, Clone)] /// Represents a participating user in a guild. @@ -17,12 +19,14 @@ pub struct GuildMember { pub nick: Option, pub avatar: Option, pub roles: Vec, - pub joined_at: String, + #[serde(with = "ts_seconds_str")] + pub joined_at: DateTime, pub premium_since: Option, pub deaf: bool, pub mute: bool, pub flags: Option, pub pending: Option, pub permissions: Option, - pub communication_disabled_until: Option, + #[serde(with = "ts_seconds_option_str")] + pub communication_disabled_until: Option>, } diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs index e03a078..607ccf2 100644 --- a/src/types/entities/message.rs +++ b/src/types/entities/message.rs @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::{ @@ -10,7 +11,7 @@ use crate::types::{ Application, Attachment, Channel, Emoji, GuildMember, PublicUser, RoleSubscriptionData, Sticker, StickerItem, User, }, - utils::Snowflake, + utils::{Snowflake, serde::*}, }; #[derive(Debug, Serialize, Deserialize, Default, Clone)] @@ -25,8 +26,10 @@ pub struct Message { #[cfg_attr(feature = "sqlx", sqlx(skip))] pub author: Option, pub content: Option, - pub timestamp: String, - pub edited_timestamp: Option, + #[serde(with = "ts_seconds_str")] + pub timestamp: DateTime, + #[serde(with = "ts_seconds_option_str")] + pub edited_timestamp: Option>, pub tts: Option, pub mention_everyone: bool, #[cfg_attr(feature = "sqlx", sqlx(skip))] diff --git a/src/types/schema/auth.rs b/src/types/schema/auth.rs index 2796805..c1c1f55 100644 --- a/src/types/schema/auth.rs +++ b/src/types/schema/auth.rs @@ -2,7 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use crate::types::utils::serde::ts_seconds_option_str; #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -13,7 +15,8 @@ pub struct RegisterSchema { pub email: Option, pub fingerprint: Option, pub invite: Option, - pub date_of_birth: Option, + #[serde(with = "ts_seconds_option_str")] + pub date_of_birth: Option>, pub gift_code_sku_id: Option, pub captcha_key: Option, pub promotional_email_opt_in: Option, diff --git a/src/types/utils/mod.rs b/src/types/utils/mod.rs index 8879688..5608fe7 100644 --- a/src/types/utils/mod.rs +++ b/src/types/utils/mod.rs @@ -11,3 +11,5 @@ pub mod jwt; mod regexes; mod rights; mod snowflake; +pub mod serde; + diff --git a/src/types/utils/serde.rs b/src/types/utils/serde.rs new file mode 100644 index 0000000..28d6e60 --- /dev/null +++ b/src/types/utils/serde.rs @@ -0,0 +1,263 @@ +use core::fmt; +use chrono::{LocalResult, NaiveDateTime}; +use serde::de; +use chrono::serde::ts_seconds; + +#[doc(hidden)] +#[derive(Debug)] +pub struct SecondsStringTimestampVisitor; + + +/// Ser/de to/from timestamps in seconds +/// +/// Intended for use with `serde`'s `with` attribute. +/// +/// # Example: +/// +/// ```rust +/// # use chrono::{TimeZone, DateTime, Utc}; +/// # use serde::{Deserialize, Serialize}; +/// use chrono::serde::ts_seconds; +/// #[derive(Deserialize, Serialize)] +/// struct S { +/// #[serde(with = "ts_seconds_str")] +/// time: DateTime +/// } +/// +/// let time = Utc.with_ymd_and_hms(2015, 5, 15, 10, 0, 0).unwrap(); +/// let my_s = S { +/// time: time.clone(), +/// }; +/// +/// let as_string = serde_json::to_string(&my_s)?; +/// assert_eq!(as_string, r#"{"time":"1431684000"}"#); +/// let my_s: S = serde_json::from_str(&as_string)?; +/// assert_eq!(my_s.time, time); +/// # Ok::<(), serde_json::Error>(()) +/// ``` + +pub mod ts_seconds_str { + use core::fmt; + use chrono::{DateTime, LocalResult, Utc}; + use super::SecondsStringTimestampVisitor; + use serde::{de, ser}; + use chrono::TimeZone; + use super::serde_from; + + /// Serialize a UTC datetime into an integer number of seconds since the epoch + /// + /// Intended for use with `serde`s `serialize_with` attribute. + /// + /// # Example: + /// + /// ```rust + /// # use chrono::{TimeZone, DateTime, Utc}; + /// # use serde::Serialize; + /// use chrono::serde::ts_seconds::serialize as to_ts; + /// #[derive(Serialize)] + /// struct S { + /// #[serde(serialize_with = "ts_seconds_str")] + /// time: DateTime + /// } + /// + /// let my_s = S { + /// time: Utc.with_ymd_and_hms(2015, 5, 15, 10, 0, 0).unwrap(), + /// }; + /// let as_string = serde_json::to_string(&my_s)?; + /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); + /// # Ok::<(), serde_json::Error>(()) + /// ``` + pub fn serialize(dt: &DateTime, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_str(&format!("{}", dt.timestamp())) + } + + /// Deserialize a `DateTime` from a seconds timestamp + /// + /// Intended for use with `serde`s `deserialize_with` attribute. + /// + /// # Example: + /// + /// ```rust + /// # use chrono::{DateTime, TimeZone, Utc}; + /// # use serde::Deserialize; + /// use chrono::serde::ts_seconds::deserialize as from_ts; + /// #[derive(Debug, PartialEq, Deserialize)] + /// struct S { + /// #[serde(deserialize_with = "ts_seconds_str")] + /// time: DateTime + /// } + /// + /// let my_s: S = serde_json::from_str(r#"{ "time": "1431684000" }"#)?; + /// assert_eq!(my_s, S { time: Utc.timestamp_opt(1431684000, 0).unwrap() }); + /// # Ok::<(), serde_json::Error>(()) + /// ``` + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + d.deserialize_str(SecondsStringTimestampVisitor) + } + + impl<'de> de::Visitor<'de> for SecondsStringTimestampVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a unix timestamp in seconds") + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + serde_from(Utc.timestamp_opt(value.parse::().map_err(|e| E::custom(e))?, 0), &value) + } + } +} + +/// Ser/de to/from optional timestamps in seconds +/// +/// Intended for use with `serde`'s `with` attribute. +/// +/// # Example: +/// +/// ```rust +/// # use chrono::{TimeZone, DateTime, Utc}; +/// # use serde::{Deserialize, Serialize}; +/// use chrono::serde::ts_seconds_option; +/// #[derive(Deserialize, Serialize)] +/// struct S { +/// #[serde(with = "ts_seconds_option_str")] +/// time: Option> +/// } +/// +/// let time = Some(Utc.with_ymd_and_hms(2015, 5, 15, 10, 0, 0).unwrap()); +/// let my_s = S { +/// time: time.clone(), +/// }; +/// +/// let as_string = serde_json::to_string(&my_s)?; +/// assert_eq!(as_string, r#"{"time":"1431684000"}"#); +/// let my_s: S = serde_json::from_str(&as_string)?; +/// assert_eq!(my_s.time, time); +/// # Ok::<(), serde_json::Error>(()) +/// ``` +pub mod ts_seconds_option_str { + use core::fmt; + use chrono::{DateTime, Utc}; + use serde::{de, ser}; + use super::SecondsStringTimestampVisitor; + + /// Serialize a UTC datetime into an integer number of seconds since the epoch or none + /// + /// Intended for use with `serde`s `serialize_with` attribute. + /// + /// # Example: + /// + /// ```rust + /// # use chrono::{TimeZone, DateTime, Utc}; + /// # use serde::Serialize; + /// use chrono::serde::ts_seconds_option::serialize as to_tsopt; + /// #[derive(Serialize)] + /// struct S { + /// #[serde(serialize_with = "ts_seconds_option_str")] + /// time: Option> + /// } + /// + /// let my_s = S { + /// time: Some(Utc.with_ymd_and_hms(2015, 5, 15, 10, 0, 0).unwrap()), + /// }; + /// let as_string = serde_json::to_string(&my_s)?; + /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); + /// # Ok::<(), serde_json::Error>(()) + /// ``` + pub fn serialize(opt: &Option>, serializer: S) -> Result + where + S: ser::Serializer, + { + match *opt { + Some(ref dt) => serializer.serialize_some(&dt.timestamp().to_string()), + None => serializer.serialize_none(), + } + } + + /// Deserialize a `DateTime` from a seconds timestamp or none + /// + /// Intended for use with `serde`s `deserialize_with` attribute. + /// + /// # Example: + /// + /// ```rust + /// # use chrono::{DateTime, TimeZone, Utc}; + /// # use serde::Deserialize; + /// use chrono::serde::ts_seconds_option::deserialize as from_tsopt; + /// #[derive(Debug, PartialEq, Deserialize)] + /// struct S { + /// #[serde(deserialize_with = "ts_seconds_option_str")] + /// time: Option> + /// } + /// + /// let my_s: S = serde_json::from_str(r#"{ "time": "1431684000" }"#)?; + /// assert_eq!(my_s, S { time: Utc.timestamp_opt(1431684000, 0).single() }); + /// # Ok::<(), serde_json::Error>(()) + /// ``` + pub fn deserialize<'de, D>(d: D) -> Result>, D::Error> + where + D: de::Deserializer<'de>, + { + d.deserialize_option(OptionSecondsTimestampVisitor) + } + + struct OptionSecondsTimestampVisitor; + + impl<'de> de::Visitor<'de> for OptionSecondsTimestampVisitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a unix timestamp in seconds or none") + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_some(self, d: D) -> Result + where + D: de::Deserializer<'de>, + { + d.deserialize_str(SecondsStringTimestampVisitor).map(Some) + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + /// Deserialize a timestamp in seconds since the epoch + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } + } +} + +pub(crate) fn serde_from(me: LocalResult, ts: &V) -> Result + where + E: de::Error, + V: fmt::Display, + T: fmt::Display, +{ + // TODO: Make actual error type + match me { + LocalResult::None => Err(E::custom("value is not a legal timestamp")), + LocalResult::Ambiguous(min, max) => { + Err(E::custom("value is an ambiguous timestamp")) + } + LocalResult::Single(val) => Ok(val), + } +} \ No newline at end of file