cloud_api_types: Add more data to the `GetAuthenticatedUserResponse` (#35384)

Marshall Bowers created

This PR adds more data to the `GetAuthenticatedUserResponse`.

We now return more information about the authenticated user, as well as
their plan information.

Release Notes:

- N/A

Change summary

Cargo.lock                                      |   4 
crates/cloud_api_types/Cargo.toml               |   6 
crates/cloud_api_types/src/cloud_api_types.rs   |  26 ++
crates/cloud_api_types/src/timestamp.rs         | 166 +++++++++++++++++++
crates/cloud_llm_client/src/cloud_llm_client.rs |   4 
5 files changed, 204 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3049,7 +3049,11 @@ dependencies = [
 name = "cloud_api_types"
 version = "0.1.0"
 dependencies = [
+ "chrono",
+ "cloud_llm_client",
+ "pretty_assertions",
  "serde",
+ "serde_json",
  "workspace-hack",
 ]
 

crates/cloud_api_types/Cargo.toml 🔗

@@ -12,5 +12,11 @@ workspace = true
 path = "src/cloud_api_types.rs"
 
 [dependencies]
+chrono.workspace = true
+cloud_llm_client.workspace = true
 serde.workspace = true
 workspace-hack.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
+serde_json.workspace = true

crates/cloud_api_types/src/cloud_api_types.rs 🔗

@@ -1,14 +1,40 @@
+mod timestamp;
+
 use serde::{Deserialize, Serialize};
 
+pub use crate::timestamp::Timestamp;
+
 #[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct GetAuthenticatedUserResponse {
     pub user: AuthenticatedUser,
+    pub feature_flags: Vec<String>,
+    pub plan: PlanInfo,
 }
 
 #[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct AuthenticatedUser {
     pub id: i32,
+    pub metrics_id: String,
     pub avatar_url: String,
     pub github_login: String,
     pub name: Option<String>,
+    pub is_staff: bool,
+    pub accepted_tos_at: Option<Timestamp>,
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct PlanInfo {
+    pub plan: cloud_llm_client::Plan,
+    pub subscription_period: Option<SubscriptionPeriod>,
+    pub usage: cloud_llm_client::CurrentUsage,
+    pub trial_started_at: Option<Timestamp>,
+    pub is_usage_based_billing_enabled: bool,
+    pub is_account_too_young: bool,
+    pub has_overdue_invoices: bool,
+}
+
+#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
+pub struct SubscriptionPeriod {
+    pub started_at: Timestamp,
+    pub ended_at: Timestamp,
 }

crates/cloud_api_types/src/timestamp.rs 🔗

@@ -0,0 +1,166 @@
+use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+/// A timestamp with a serialized representation in RFC 3339 format.
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub struct Timestamp(pub DateTime<Utc>);
+
+impl Timestamp {
+    pub fn new(datetime: DateTime<Utc>) -> Self {
+        Self(datetime)
+    }
+}
+
+impl From<DateTime<Utc>> for Timestamp {
+    fn from(value: DateTime<Utc>) -> Self {
+        Self(value)
+    }
+}
+
+impl From<NaiveDateTime> for Timestamp {
+    fn from(value: NaiveDateTime) -> Self {
+        Self(value.and_utc())
+    }
+}
+
+impl Serialize for Timestamp {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let rfc3339_string = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
+        serializer.serialize_str(&rfc3339_string)
+    }
+}
+
+impl<'de> Deserialize<'de> for Timestamp {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let value = String::deserialize(deserializer)?;
+        let datetime = DateTime::parse_from_rfc3339(&value)
+            .map_err(serde::de::Error::custom)?
+            .to_utc();
+        Ok(Self(datetime))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use chrono::NaiveDate;
+    use pretty_assertions::assert_eq;
+
+    use super::*;
+
+    #[test]
+    fn test_timestamp_serialization() {
+        let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
+            .unwrap()
+            .to_utc();
+        let timestamp = Timestamp::new(datetime);
+
+        let json = serde_json::to_string(&timestamp).unwrap();
+        assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
+    }
+
+    #[test]
+    fn test_timestamp_deserialization() {
+        let json = "\"2023-12-25T14:30:45.123Z\"";
+        let timestamp: Timestamp = serde_json::from_str(json).unwrap();
+
+        let expected = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
+            .unwrap()
+            .to_utc();
+
+        assert_eq!(timestamp.0, expected);
+    }
+
+    #[test]
+    fn test_timestamp_roundtrip() {
+        let original = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
+            .unwrap()
+            .to_utc();
+
+        let timestamp = Timestamp::new(original);
+        let json = serde_json::to_string(&timestamp).unwrap();
+        let deserialized: Timestamp = serde_json::from_str(&json).unwrap();
+
+        assert_eq!(deserialized.0, original);
+    }
+
+    #[test]
+    fn test_timestamp_from_datetime_utc() {
+        let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
+            .unwrap()
+            .to_utc();
+
+        let timestamp = Timestamp::from(datetime);
+        assert_eq!(timestamp.0, datetime);
+    }
+
+    #[test]
+    fn test_timestamp_from_naive_datetime() {
+        let naive_dt = NaiveDate::from_ymd_opt(2023, 12, 25)
+            .unwrap()
+            .and_hms_milli_opt(14, 30, 45, 123)
+            .unwrap();
+
+        let timestamp = Timestamp::from(naive_dt);
+        let expected = naive_dt.and_utc();
+
+        assert_eq!(timestamp.0, expected);
+    }
+
+    #[test]
+    fn test_timestamp_serialization_with_microseconds() {
+        // Test that microseconds are truncated to milliseconds
+        let datetime = NaiveDate::from_ymd_opt(2023, 12, 25)
+            .unwrap()
+            .and_hms_micro_opt(14, 30, 45, 123456)
+            .unwrap()
+            .and_utc();
+
+        let timestamp = Timestamp::new(datetime);
+        let json = serde_json::to_string(&timestamp).unwrap();
+
+        // Should be truncated to milliseconds
+        assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
+    }
+
+    #[test]
+    fn test_timestamp_deserialization_without_milliseconds() {
+        let json = "\"2023-12-25T14:30:45Z\"";
+        let timestamp: Timestamp = serde_json::from_str(json).unwrap();
+
+        let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
+            .unwrap()
+            .and_hms_opt(14, 30, 45)
+            .unwrap()
+            .and_utc();
+
+        assert_eq!(timestamp.0, expected);
+    }
+
+    #[test]
+    fn test_timestamp_deserialization_with_timezone() {
+        let json = "\"2023-12-25T14:30:45.123+05:30\"";
+        let timestamp: Timestamp = serde_json::from_str(json).unwrap();
+
+        // Should be converted to UTC
+        let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
+            .unwrap()
+            .and_hms_milli_opt(9, 0, 45, 123) // 14:30:45 + 5:30 = 20:00:45, but we want UTC so subtract 5:30
+            .unwrap()
+            .and_utc();
+
+        assert_eq!(timestamp.0, expected);
+    }
+
+    #[test]
+    fn test_timestamp_deserialization_with_invalid_format() {
+        let json = "\"invalid-date\"";
+        let result: Result<Timestamp, _> = serde_json::from_str(json);
+        assert!(result.is_err());
+    }
+}

crates/cloud_llm_client/src/cloud_llm_client.rs 🔗

@@ -308,13 +308,13 @@ pub struct GetSubscriptionResponse {
     pub usage: Option<CurrentUsage>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct CurrentUsage {
     pub model_requests: UsageData,
     pub edit_predictions: UsageData,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct UsageData {
     pub used: u32,
     pub limit: UsageLimit,