chiark / gitweb /
Tolerate hidden number of followers in Account
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 2 Feb 2024 14:45:33 +0000 (14:45 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 2 Feb 2024 23:52:42 +0000 (23:52 +0000)
This can be -1.  Display `(hidden)` when it's not available.

src/types.rs

index ccc8c030be9183077cc8111c17f595f4ab4542d0..e3b653a1138a1b77ecd987dba2e735c34ac4f9c8 100644 (file)
@@ -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<DateTime<Utc>>,
 }
 
+/// Value that came from server (as JSON) that was supposedly a u64
+///
+/// Apparently some servers sometimes send negative numbers for these,
+/// meaning unavailable:
+///
+/// <https://glitch-soc.github.io/docs/features/hide-follower-count/>
+//
+// 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<u64> since that's two words and this is one
+pub struct FollowersCount(i64);
+
+impl TryFrom<i64> for FollowersCount {
+    type Error = String;
+    fn try_from(i: i64) -> Result<FollowersCount, Self::Error> {
+        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::<S>(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<ApproxDate>,
     pub statuses_count: u64,
-    pub followers_count: u64,
+    pub followers_count: FollowersCount,
     pub following_count: u64,
 
     // In the wire protocol, 'CredentialAccount' is a subclass of