#![allow(clippy::struct_excessive_bools)]
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserResponse {
pub data: User,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct City {
pub country_code: Option<String>,
pub name: Option<String>,
pub state: Option<String>,
pub title: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub username: String,
pub display_name: String,
pub full_name: Option<String>,
pub bio: Option<String>,
pub email: Option<String>,
pub public_email: Option<String>,
pub photo: Option<String>,
pub timezone: String,
pub website: Option<String>,
pub human_readable_website: Option<String>,
pub plan: Option<String>,
#[serde(default)]
pub has_premium_features: bool,
#[serde(default)]
pub is_email_public: bool,
#[serde(default)]
pub is_photo_public: bool,
#[serde(default)]
pub is_email_confirmed: bool,
#[serde(default)]
pub is_hireable: bool,
#[serde(default)]
pub logged_time_public: bool,
#[serde(default)]
pub languages_used_public: bool,
#[serde(default)]
pub editors_used_public: bool,
#[serde(default)]
pub categories_used_public: bool,
#[serde(default)]
pub os_used_public: bool,
pub last_heartbeat_at: Option<String>,
pub last_plugin: Option<String>,
pub last_plugin_name: Option<String>,
pub last_project: Option<String>,
pub last_branch: Option<String>,
pub city: Option<City>,
pub github_username: Option<String>,
pub twitter_username: Option<String>,
pub linkedin_username: Option<String>,
pub wonderfuldev_username: Option<String>,
pub location: Option<String>,
pub profile_url: Option<String>,
#[serde(default)]
pub writes_only: bool,
pub timeout: Option<u32>,
pub time_format_24hr: Option<bool>,
pub created_at: String,
pub modified_at: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummaryResponse {
pub data: Vec<SummaryData>,
pub end: String,
pub start: String,
#[serde(default)]
pub cumulative_total: Option<CumulativeTotal>,
#[serde(default)]
pub daily_average: Option<DailyAverage>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CumulativeTotal {
pub seconds: f64,
pub text: String,
#[serde(default)]
pub decimal: String,
#[serde(default)]
pub digital: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DailyAverage {
#[serde(default)]
pub holidays: u32,
#[serde(default)]
pub days_including_holidays: u32,
#[serde(default)]
pub days_minus_holidays: u32,
pub seconds: f64,
pub text: String,
pub seconds_including_other_language: f64,
pub text_including_other_language: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SummaryData {
#[serde(default)]
pub categories: Vec<SummaryEntry>,
#[serde(default)]
pub dependencies: Vec<SummaryEntry>,
#[serde(default)]
pub editors: Vec<SummaryEntry>,
pub grand_total: GrandTotal,
#[serde(default)]
pub languages: Vec<SummaryEntry>,
#[serde(default)]
pub machines: Vec<MachineEntry>,
#[serde(default)]
pub operating_systems: Vec<SummaryEntry>,
#[serde(default)]
pub projects: Vec<SummaryEntry>,
#[serde(default)]
pub branches: Vec<SummaryEntry>,
#[serde(default)]
pub entities: Vec<SummaryEntry>,
pub range: SummaryRange,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SummaryEntry {
pub digital: String,
pub hours: u32,
pub minutes: u32,
pub name: String,
pub percent: f64,
#[serde(default)]
pub seconds: u32,
pub text: String,
pub total_seconds: f64,
#[serde(default)]
pub ai_additions: u32,
#[serde(default)]
pub ai_deletions: u32,
#[serde(default)]
pub human_additions: u32,
#[serde(default)]
pub human_deletions: u32,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MachineEntry {
pub digital: String,
pub hours: u32,
pub machine_name_id: String,
pub minutes: u32,
pub name: String,
pub percent: f64,
#[serde(default)]
pub seconds: u32,
pub text: String,
pub total_seconds: f64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GrandTotal {
pub digital: String,
pub hours: u32,
pub minutes: u32,
#[serde(default)]
pub seconds: u32,
pub text: String,
pub total_seconds: f64,
#[serde(default)]
pub ai_additions: u32,
#[serde(default)]
pub ai_deletions: u32,
#[serde(default)]
pub human_additions: u32,
#[serde(default)]
pub human_deletions: u32,
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SummaryRange {
pub date: Option<String>,
pub end: String,
pub start: String,
pub text: Option<String>,
pub timezone: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectsResponse {
pub data: Vec<Project>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
pub badge: Option<String>,
pub color: Option<String>,
pub created_at: String,
pub has_public_url: bool,
pub human_readable_last_heartbeat_at: String,
pub human_readable_first_heartbeat_at: Option<String>,
pub id: String,
pub last_heartbeat_at: String,
pub first_heartbeat_at: Option<String>,
pub name: String,
pub repository: Option<String>,
pub url: Option<String>,
pub urlencoded_name: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatsResponse {
pub data: Stats,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stats {
pub best_day: Option<BestDay>,
pub categories: Vec<SummaryEntry>,
pub created_at: String,
pub daily_average: f64,
pub daily_average_including_other_language: f64,
pub days_including_holidays: u32,
pub days_minus_holidays: u32,
#[serde(default)]
pub dependencies: Vec<SummaryEntry>,
pub editors: Vec<SummaryEntry>,
pub end: String,
pub holidays: u32,
pub human_readable_daily_average: String,
#[serde(default)]
pub human_readable_daily_average_including_other_language: String,
pub human_readable_range: String,
pub human_readable_total: String,
#[serde(default)]
pub human_readable_total_including_other_language: String,
#[serde(default)]
pub ai_additions: u32,
#[serde(default)]
pub ai_deletions: u32,
#[serde(default)]
pub human_additions: u32,
#[serde(default)]
pub human_deletions: u32,
pub id: Option<String>,
#[serde(default)]
pub is_already_updating: bool,
#[serde(default)]
pub is_coding_activity_visible: bool,
#[serde(default)]
pub is_language_usage_visible: bool,
#[serde(default)]
pub is_editor_usage_visible: bool,
#[serde(default)]
pub is_category_usage_visible: bool,
#[serde(default)]
pub is_os_usage_visible: bool,
#[serde(default)]
pub is_stuck: bool,
#[serde(default)]
pub is_including_today: bool,
pub is_up_to_date: bool,
pub languages: Vec<SummaryEntry>,
pub machines: Vec<MachineEntry>,
pub modified_at: Option<String>,
pub operating_systems: Vec<SummaryEntry>,
pub percent_calculated: u32,
pub projects: Vec<SummaryEntry>,
pub range: String,
pub start: String,
pub status: String,
pub timeout: u32,
pub timezone: String,
pub total_seconds: f64,
pub total_seconds_including_other_language: f64,
pub user_id: String,
pub username: String,
pub writes_only: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BestDay {
pub date: String,
pub text: String,
pub total_seconds: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalsResponse {
pub data: Vec<Goal>,
pub total: u32,
pub total_pages: u32,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalOwner {
pub display_name: Option<String>,
pub email: Option<String>,
pub full_name: Option<String>,
pub id: String,
pub photo: Option<String>,
pub username: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalSharedWith {
pub display_name: Option<String>,
pub email: Option<String>,
pub full_name: Option<String>,
pub id: Option<String>,
pub photo: Option<String>,
pub status: Option<String>,
pub user_id: Option<String>,
pub username: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalSubscriber {
pub email: Option<String>,
pub email_frequency: Option<String>,
pub full_name: Option<String>,
pub user_id: String,
pub username: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Goal {
pub average_status: Option<String>,
#[serde(default)]
pub chart_data: Option<Vec<GoalChartEntry>>,
pub created_at: String,
pub cumulative_status: Option<String>,
pub custom_title: Option<String>,
pub delta: String,
#[serde(default)]
pub editors: Vec<String>,
pub id: String,
#[serde(default)]
pub ignore_days: Vec<String>,
pub ignore_zero_days: bool,
pub improve_by_percent: Option<f64>,
#[serde(default)]
pub is_current_user_owner: bool,
pub is_enabled: bool,
pub is_inverse: bool,
pub is_snoozed: bool,
pub is_tweeting: bool,
#[serde(default)]
pub languages: Vec<String>,
pub modified_at: Option<String>,
pub owner: Option<GoalOwner>,
#[serde(default)]
pub projects: Vec<String>,
pub range_text: Option<String>,
pub range_status: Option<String>,
pub range_status_reason: Option<String>,
pub seconds: f64,
#[serde(default)]
pub shared_with: Vec<GoalSharedWith>,
pub snooze_until: Option<String>,
pub status: String,
#[serde(default)]
pub status_percent_calculated: u32,
#[serde(default)]
pub subscribers: Vec<GoalSubscriber>,
pub title: String,
#[serde(rename = "type")]
pub goal_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalChartEntry {
pub actual_seconds: f64,
pub actual_seconds_text: String,
pub goal_seconds: f64,
pub goal_seconds_text: String,
pub range: GoalChartRange,
pub range_status: String,
pub range_status_reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalChartRange {
pub date: Option<String>,
pub end: String,
pub start: String,
pub text: String,
pub timezone: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaderboardResponse {
pub current_user: Option<LeaderboardEntry>,
pub data: Vec<LeaderboardEntry>,
pub language: Option<String>,
pub modified_at: Option<String>,
#[serde(default)]
pub page: u32,
pub range: Option<LeaderboardRange>,
#[serde(default)]
pub timeout: u32,
#[serde(default)]
pub total_pages: u32,
#[serde(default)]
pub writes_only: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaderboardEntry {
pub rank: Option<u32>,
pub running_total: RunningTotal,
pub user: LeaderboardUser,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunningTotal {
pub daily_average: f64,
pub human_readable_daily_average: String,
pub human_readable_total: String,
#[serde(default)]
pub languages: Vec<SummaryEntry>,
pub modified_at: Option<String>,
pub total_seconds: f64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaderboardUser {
pub display_name: String,
pub email: Option<String>,
pub full_name: Option<String>,
pub human_readable_website: Option<String>,
pub id: String,
pub is_email_public: bool,
pub is_hireable: bool,
pub location: Option<String>,
pub photo: Option<String>,
pub photo_public: bool,
pub profile_url: Option<String>,
pub public_email: Option<String>,
pub username: Option<String>,
pub website: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeaderboardRange {
pub end_date: String,
pub end_text: String,
pub name: String,
pub start_date: String,
pub start_text: String,
pub text: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grand_total_deserialize() {
let json = r#"{
"digital": "6:42",
"hours": 6,
"minutes": 42,
"seconds": 0,
"text": "6 hrs 42 mins",
"total_seconds": 24120.0
}"#;
let gt: GrandTotal = serde_json::from_str(json).unwrap();
assert_eq!(gt.hours, 6);
assert_eq!(gt.minutes, 42);
#[allow(clippy::float_cmp)]
{
assert_eq!(gt.total_seconds, 24120.0);
}
assert_eq!(gt.text, "6 hrs 42 mins");
}
#[test]
fn summary_entry_ignores_unknown_fields() {
let json = r#"{
"digital": "3:30",
"hours": 3,
"minutes": 30,
"name": "Python",
"percent": 52.3,
"seconds": 0,
"text": "3 hrs 30 mins",
"total_seconds": 12600.0,
"some_future_field": "ignored"
}"#;
let entry: SummaryEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.name, "Python");
assert_eq!(entry.hours, 3);
}
#[test]
fn summary_data_default_dependencies() {
let json = r#"{
"categories": [],
"editors": [],
"grand_total": {
"digital": "0:00",
"hours": 0,
"minutes": 0,
"seconds": 0,
"text": "0 secs",
"total_seconds": 0.0
},
"languages": [],
"operating_systems": [],
"projects": [],
"range": {
"end": "2025-01-14T00:00:00Z",
"start": "2025-01-13T00:00:00Z"
}
}"#;
let data: SummaryData = serde_json::from_str(json).unwrap();
assert!(data.dependencies.is_empty());
assert!(data.machines.is_empty());
}
#[test]
fn user_roundtrip() {
let user = User {
id: "abc-123".to_owned(),
username: "johndoe".to_owned(),
display_name: "John Doe".to_owned(),
full_name: Some("John Doe".to_owned()),
bio: None,
email: None,
public_email: None,
photo: None,
timezone: "UTC".to_owned(),
website: None,
human_readable_website: None,
plan: Some("free".to_owned()),
has_premium_features: false,
is_email_public: false,
is_photo_public: false,
is_email_confirmed: true,
is_hireable: false,
logged_time_public: false,
languages_used_public: false,
editors_used_public: false,
categories_used_public: false,
os_used_public: false,
last_heartbeat_at: None,
last_plugin: None,
last_plugin_name: None,
last_project: None,
last_branch: None,
city: None,
github_username: None,
twitter_username: None,
linkedin_username: None,
wonderfuldev_username: None,
location: None,
profile_url: Some("https://wakatime.com/@johndoe".to_owned()),
writes_only: false,
timeout: Some(15),
time_format_24hr: Some(false),
created_at: "2024-01-01T00:00:00Z".to_owned(),
modified_at: None,
};
let json = serde_json::to_string(&user).unwrap();
let user2: User = serde_json::from_str(&json).unwrap();
assert_eq!(user.id, user2.id);
assert_eq!(user.username, user2.username);
assert_eq!(user.timezone, user2.timezone);
}
}