From aedccaf8622e26a53c44d50cc9ba0822ea387f8a Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Fri, 2 Feb 2024 23:40:26 +0000 Subject: [PATCH] Tolerate hidden number of followers in Account (redux) Introduce a bespoke type which allows (just) -1. --- src/text.rs | 11 +++------ src/types.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/text.rs b/src/text.rs index 25b8b87..be1ab9e 100644 --- a/src/text.rs +++ b/src/text.rs @@ -3,7 +3,6 @@ use chrono::NaiveDateTime; use chrono::{DateTime, Local, Utc}; use core::cmp::{max, min}; use std::collections::{BTreeMap, HashSet}; -use std::fmt::Display; use unicode_width::UnicodeWidthStr; use super::client::{Client, ClientError}; @@ -3028,13 +3027,9 @@ impl ExamineUserDisplay { let post_count = Paragraph::new().add(ColouredString::plain( &format!("Number of posts: {}", ac.statuses_count), )); - let followers_count = Paragraph::new().add(ColouredString::plain(&{ - let followers_count = u64::try_from(ac.followers_count); - format!("Number of followers: {}", match &followers_count { - Ok(y) => y as &dyn Display, - Err(_) => &"(hidden)", - }) - })); + let followers_count = Paragraph::new().add(ColouredString::plain( + &format!("Number of followers: {}", ac.followers_count), + )); let following_count = Paragraph::new().add(ColouredString::plain( &format!("Number of users followed: {}", ac.following_count), )); diff --git a/src/types.rs b/src/types.rs index 56f5532..e3b653a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; use std::boxed::Box; +use std::fmt::{self, Display}; use std::option::Option; use super::coloured_string::ColouredString; @@ -12,6 +13,71 @@ pub struct AccountField { pub verified_at: Option>, } +/// Value that came from server (as JSON) that was supposedly a u64 +/// +/// Apparently some servers sometimes send negative numbers for these, +/// meaning unavailable: +/// +/// +// +// If it turns out that servers send other values eg "null" +// or absent fields, or that such toleration is needed for other +// informational fields from servers, we should make this type +// more relaxed and/or rename it +// (and we may need to apply #[serde(default)] to fields of this type). +#[derive(Deserialize, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +#[serde(try_from = "i64")] +// Not Option since that's two words and this is one +pub struct FollowersCount(i64); + +impl TryFrom for FollowersCount { + type Error = String; + fn try_from(i: i64) -> Result { + if i >= -1 { + Ok(FollowersCount(i)) + } else { + Err("unexpected nonnegative value, would tolerate -1".into()) + } + } +} + +impl Display for FollowersCount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0 >= 0 { + Display::fmt(&self.0, f) + } else if self.0 == -1 { + write!(f, "unavailable") + } else { + // shouldn't happen but let's not panic + write!(f, "unavailable ({})", self.0) + } + } +} + +#[test] +fn test_supposedly_non_negative() { + #[derive(Deserialize, Debug)] + struct S { + a: FollowersCount, + } + + let chk = |j, exp_0, exp_display| { + let s: S = serde_json::from_str(j).unwrap(); + assert_eq!(s.a.0, exp_0); + assert_eq!(s.a.to_string(), exp_display); + }; + let chk_err = |j| { + serde_json::from_str::(j).unwrap_err(); + }; + + chk(r#"{ "a": 0 }"#, 0, "0"); + chk(r#"{ "a": 1 }"#, 1, "1"); + chk(r#"{ "a": -1 }"#, -1, "unavailable"); + chk_err(r#"{}"#); + chk_err(r#"{ "a": -42 }"#); + chk_err(r#"{ "a": null }"#); +} + // 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: @@ -123,7 +189,7 @@ pub struct Account { pub created_at: ApproxDate, pub last_status_at: Option, pub statuses_count: u64, - pub followers_count: i64, + pub followers_count: FollowersCount, pub following_count: u64, // In the wire protocol, 'CredentialAccount' is a subclass of -- 2.30.2