chiark / gitweb /
Sorted out the approximate dates in Account.
authorSimon Tatham <anakin@pobox.com>
Mon, 1 Jan 2024 11:31:37 +0000 (11:31 +0000)
committerSimon Tatham <anakin@pobox.com>
Mon, 1 Jan 2024 11:36:42 +0000 (11:36 +0000)
I couldn't parse last_status_at as a DateTime<Utc> 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

index 8f0d43ad47e7f37b263001f053d0750ce5abe217..0ff35e7aee5c85f52572875e05fd0ad574802fa2 100644 (file)
@@ -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<DateTime<Utc>>,
 }
 
-#[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<D>(deserializer: D) -> Result<Self, D::Error>
+        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<E>(self, orig_value: &str) -> Result<Self::Value, E>
+                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<ApproxDate> works too
+    let date: Option<ApproxDate> = 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<ApproxDate> = 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<Box<Account>>,
     pub suspended: Option<bool>,
     pub limited: Option<bool>,
-    pub created_at: DateTime<Utc>,
-    pub last_status_at: Option<String>, // lacks a timezone, so serde can't
-                                        // deserialize it in the obvious way
+    pub created_at: ApproxDate,
+    pub last_status_at: Option<ApproxDate>,
     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<String>,
 }
 
-#[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<String>,
 }
 
-#[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<Vec<FilterResult>>,
 }
 
-#[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,