-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,
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,
#[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,
#[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,
pub description: Option<String>,
}
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Deserialize, Debug, Clone)]
pub struct StatusMention {
pub id: String,
pub username: String,
pub acct: String,
}
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Deserialize, Debug, Clone)]
pub struct Status {
pub id: String,
pub uri: String,
// 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,
#[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,