From 23d7230d33db4ad5efa6dd751e4851dd47ccb502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 23 May 2024 10:01:46 +0100 Subject: [PATCH] make sure 'OffsetDateTimeJsonSchemaWrapper' is serialised with legacy format --- Cargo.lock | 1 + nym-api/nym-api-requests/Cargo.toml | 4 + nym-api/nym-api-requests/src/models.rs | 139 +++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57bbd60ad5..64ce690dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4017,6 +4017,7 @@ dependencies = [ "nym-node-requests", "schemars", "serde", + "serde_json", "tendermint", "time", "ts-rs", diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 42f2495bcd..44d43e855d 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -27,6 +27,10 @@ nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"] nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } + +[dev-dependencies] +serde_json.workspace = true + [features] default = [] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index a8cbd19b84..dd7c0a446a 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -420,15 +420,110 @@ where Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch())) } +pub(crate) mod overengineered_offset_date_time_serde { + use crate::models::unix_epoch; + use serde::de::Visitor; + use serde::ser::Error; + use serde::{Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + use time::format_description::well_known::Rfc3339; + use time::format_description::{modifier, BorrowedFormatItem, Component}; + use time::OffsetDateTime; + + struct OffsetDateTimeVisitor; + + // copied from time library because they keep it private -.- + const DEFAULT_OFFSET_DATE_TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Compound(DATE_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(TIME_FORMAT), + BorrowedFormatItem::Literal(b" "), + BorrowedFormatItem::Compound(UTC_OFFSET_FORMAT), + ]; + + const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), + ]; + + const TIME_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Hour(modifier::Hour::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Minute(modifier::Minute::default())), + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::Second(modifier::Second::default())), + BorrowedFormatItem::Literal(b"."), + BorrowedFormatItem::Component(Component::Subsecond(modifier::Subsecond::default())), + ]; + + const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::OffsetHour({ + let mut m = modifier::OffsetHour::default(); + m.sign_is_mandatory = true; + m + })), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetMinute( + modifier::OffsetMinute::default(), + )), + BorrowedFormatItem::Optional(&BorrowedFormatItem::Compound(&[ + BorrowedFormatItem::Literal(b":"), + BorrowedFormatItem::Component(Component::OffsetSecond( + modifier::OffsetSecond::default(), + )), + ])), + ])), + ]; + + impl<'de> Visitor<'de> for OffsetDateTimeVisitor { + type Value = OffsetDateTime; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("an rfc3339 or human-readable `OffsetDateTime`") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + // first try rfc3339, if that fails use default human-readable impl from time, + // finally fallback to default unix epoch + Ok(OffsetDateTime::parse(v, &Rfc3339).unwrap_or_else(|_| { + OffsetDateTime::parse(v, &DEFAULT_OFFSET_DATE_TIME_FORMAT) + .unwrap_or_else(|_| unix_epoch()) + })) + } + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(OffsetDateTimeVisitor) + } + + pub(crate) fn serialize(datetime: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DEFAULT_OFFSET_DATE_TIME_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + // for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct OffsetDateTimeJsonSchemaWrapper( - #[serde( - default = "unix_epoch", - serialize_with = "time::serde::rfc3339::serialize", - deserialize_with = "de_rfc3339_or_default" - )] - pub OffsetDateTime, + #[serde(default = "unix_epoch", with = "overengineered_offset_date_time_serde")] + pub OffsetDateTime, ); impl Default for OffsetDateTimeJsonSchemaWrapper { @@ -599,3 +694,33 @@ pub struct PartialTestResult { pub type MixnodeTestResultResponse = PaginatedResponse; pub type GatewayTestResultResponse = PaginatedResponse; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn offset_date_time_json_schema_wrapper_serde_backwards_compat() { + let mut dummy = OffsetDateTimeJsonSchemaWrapper::default(); + dummy.0 += Duration::from_millis(1); + let ser = serde_json::to_string(&dummy).unwrap(); + + assert_eq!("\"1970-01-01 00:00:00.001 +00:00:00\"", ser); + + let human_readable = "\"2024-05-23 07:41:02.756283766 +00:00:00\""; + let rfc3339 = "\"2002-10-02T15:00:00Z\""; + let rfc3339_offset = "\"2002-10-02T10:00:00-05:00\""; + + let de = serde_json::from_str::(human_readable).unwrap(); + assert_eq!(de.0.unix_timestamp(), 1716450062); + + let de = serde_json::from_str::(rfc3339).unwrap(); + assert_eq!(de.0.unix_timestamp(), 1033570800); + + let de = serde_json::from_str::(rfc3339_offset).unwrap(); + assert_eq!(de.0.unix_timestamp(), 1033570800); + + let de = serde_json::from_str::("\"nonsense\"").unwrap(); + assert_eq!(de.0.unix_timestamp(), 0); + } +}