From 15c4ba162f16bda34d6d23d40a657f6ea3eae762 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Mon, 1 Jan 2024 11:31:37 +0000 Subject: [PATCH] Sorted out the approximate dates in Account. I couldn't parse last_status_at as a DateTime in all cases at all, and even in created_at, the time part was clearly nonsense. chrono has an actual type for this, so let's use it! With bonus Baby's First Custom Deserialize Impl. --- src/types.rs | 103 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 15 deletions(-) diff --git a/src/types.rs b/src/types.rs index 8f0d43a..0ff35e7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,16 +1,90 @@ -use chrono::{DateTime,Utc}; -use serde::{Deserialize, Serialize}; +use chrono::{DateTime, NaiveDate, Utc}; +use serde::{Deserialize}; use std::boxed::Box; use std::option::Option; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct AccountField { pub name: String, pub value: String, pub verified_at: Option>, } -#[derive(Serialize, Deserialize, Debug, Clone)] +// Special type wrapping chrono::NaiveDate. This is for use in the +// Account fields 'created_at' and 'last_status_at', which I've +// observed do not in fact contain full sensible ISO 8601 timestamps: +// last_status_at is just a YYYY-MM-DD date with no time or timezone, +// and created_at _looks_ like a full timestamp but the time part is +// filled in as 00:00:00 regardless. So I think the best we can do is +// to parse both of them as a NaiveDate. +// +// But if you just ask serde to expect a NaiveDate, it will complain +// if it _does_ see the T00:00:00Z suffix. So here I have a custom +// deserializer which strips that off. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApproxDate(pub NaiveDate); + +impl<'de> Deserialize<'de> for ApproxDate { + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> + { + struct StrVisitor; + impl<'de> serde::de::Visitor<'de> for StrVisitor { + type Value = ApproxDate; + fn expecting(&self, formatter: &mut std::fmt::Formatter) + -> std::fmt::Result + { + formatter.write_str("a date in ISO 8601 format") + } + fn visit_str(self, orig_value: &str) -> Result + where E: serde::de::Error + { + let value = match orig_value.split_once('T') { + Some((before, _after)) => before, + None => orig_value, + }; + match NaiveDate::parse_from_str(value, "%Y-%m-%d") { + Ok(date) => Ok(ApproxDate(date)), + Err(_) => Err(E::custom(format!( + "couldn't get an ISO 8601 date from '{orig_value}'"))), + } + } + } + deserializer.deserialize_str(StrVisitor{}) + } +} + +#[test] +fn test_approx_date() { + // Works with just the YYYY-MM-DD date string + let date: ApproxDate = serde_json::from_str("\"2023-09-20\"").unwrap(); + assert_eq!(date, ApproxDate( + NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())); + + // Works with a T00:00:00 suffix + let date: ApproxDate = serde_json::from_str( + "\"2023-09-20T00:00:00\"").unwrap(); + assert_eq!(date, ApproxDate( + NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())); + + // Works with that _and_ the Z for timezone + let date: ApproxDate = serde_json::from_str( + "\"2023-09-20T00:00:00Z\"").unwrap(); + assert_eq!(date, ApproxDate( + NaiveDate::from_ymd_opt(2023, 9, 20).unwrap())); + + // Deserializing as an Option works too + let date: Option = serde_json::from_str( + "\"2023-09-20T00:00:00\"").unwrap(); + assert_eq!(date, Some(ApproxDate( + NaiveDate::from_ymd_opt(2023, 9, 20).unwrap()))); + + // And if you give it JSON 'null' in place of a string it gives None + let date: Option = serde_json::from_str("null").unwrap(); + assert_eq!(date, None); +} + +#[derive(Deserialize, Debug, Clone)] pub struct Account { pub id: String, pub username: String, @@ -32,21 +106,20 @@ pub struct Account { pub moved: Option>, pub suspended: Option, pub limited: Option, - pub created_at: DateTime, - pub last_status_at: Option, // lacks a timezone, so serde can't - // deserialize it in the obvious way + pub created_at: ApproxDate, + pub last_status_at: Option, pub statuses_count: u64, pub followers_count: u64, pub following_count: u64, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct Application { pub name: String, pub website: Option, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum Visibility { #[serde(rename = "public")] Public, #[serde(rename = "unlisted")] Unlisted, @@ -54,7 +127,7 @@ pub enum Visibility { #[serde(rename = "direct")] Direct, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum MediaType { #[serde(rename = "unknown")] Unknown, #[serde(rename = "image")] Image, @@ -63,7 +136,7 @@ pub enum MediaType { #[serde(rename = "audio")] Audio, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct MediaAttachment { pub id: String, #[serde(rename="type")] pub mediatype: MediaType, @@ -73,7 +146,7 @@ pub struct MediaAttachment { pub description: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct StatusMention { pub id: String, pub username: String, @@ -81,7 +154,7 @@ pub struct StatusMention { pub acct: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct Status { pub id: String, pub uri: String, @@ -116,7 +189,7 @@ pub struct Status { // pub filtered: Option>, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub enum NotificationType { #[serde(rename = "mention")] Mention, #[serde(rename = "status")] Status, @@ -130,7 +203,7 @@ pub enum NotificationType { #[serde(rename = "admin.report")] AdminReport, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone)] pub struct Notification { pub id: String, #[serde(rename="type")] pub ntype: NotificationType, -- 2.30.2