Skip to main content

waka_api/
types.rs

1//! `WakaTime` API response types.
2//!
3//! All types derive [`Debug`], [`Clone`], [`serde::Serialize`], and
4//! [`serde::Deserialize`]. Unknown JSON fields are silently ignored via
5//! `#[serde(deny_unknown_fields)]` is **not** used — this keeps the client
6//! forward-compatible with new API fields.
7
8// The WakaTime API returns several structs with more than 3 boolean fields
9// (e.g. Goal has 5, Stats has 8). These fields mirror the upstream API
10// exactly and cannot be meaningfully replaced with enums or bitflags.
11#![allow(clippy::struct_excessive_bools)]
12
13use serde::{Deserialize, Serialize};
14
15// ─────────────────────────────────────────────────────────────────────────────
16// User
17// ─────────────────────────────────────────────────────────────────────────────
18
19/// Top-level envelope returned by `GET /users/current`.
20#[non_exhaustive]
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct UserResponse {
23    /// The user object.
24    pub data: User,
25}
26
27/// Geographic location returned on user profiles and leaderboards.
28#[non_exhaustive]
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct City {
31    /// Two-letter country code (e.g. `"US"`, `"UK"`).
32    pub country_code: Option<String>,
33    /// City name (e.g. `"San Francisco"`).
34    pub name: Option<String>,
35    /// State/province name (e.g. `"California"`).
36    pub state: Option<String>,
37    /// Human-readable `"City, State"` or country if state matches city.
38    pub title: Option<String>,
39}
40
41/// A `WakaTime` user account.
42#[non_exhaustive]
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct User {
45    /// Unique user identifier (UUID).
46    pub id: String,
47    /// The user's login handle.
48    pub username: String,
49    /// Human-readable display name (full name or `@username`).
50    pub display_name: String,
51    /// Full legal name (may be `None` if not set).
52    pub full_name: Option<String>,
53    /// Optional user-defined bio.
54    pub bio: Option<String>,
55    /// Email address (only present when the authenticated user requests their
56    /// own profile; requires `email` scope).
57    pub email: Option<String>,
58    /// Public-facing email (distinct from the account email).
59    pub public_email: Option<String>,
60    /// URL of the user's avatar image.
61    pub photo: Option<String>,
62    /// IANA timezone string (e.g. `"America/New_York"`).
63    pub timezone: String,
64    /// Personal website URL.
65    pub website: Option<String>,
66    /// Human-readable website URL (without protocol prefix).
67    pub human_readable_website: Option<String>,
68    /// Subscription plan (e.g. `"free"`, `"premium"`).
69    pub plan: Option<String>,
70    /// Whether the user has access to premium features.
71    #[serde(default)]
72    pub has_premium_features: bool,
73    /// Whether the user's email address is publicly visible on leaderboards.
74    #[serde(default)]
75    pub is_email_public: bool,
76    /// Whether the user's avatar is publicly visible on leaderboards.
77    #[serde(default)]
78    pub is_photo_public: bool,
79    /// Whether the user's email address has been verified.
80    #[serde(default)]
81    pub is_email_confirmed: bool,
82    /// Whether the user is open to work (shows "hireable" badge on profile).
83    #[serde(default)]
84    pub is_hireable: bool,
85    /// Whether total coding time is visible on the public leaderboard.
86    #[serde(default)]
87    pub logged_time_public: bool,
88    /// Whether language usage is visible on the public profile.
89    #[serde(default)]
90    pub languages_used_public: bool,
91    /// Whether editor usage is visible on the public profile.
92    #[serde(default)]
93    pub editors_used_public: bool,
94    /// Whether category usage is visible on the public profile.
95    #[serde(default)]
96    pub categories_used_public: bool,
97    /// Whether operating system usage is visible on the public profile.
98    #[serde(default)]
99    pub os_used_public: bool,
100    /// ISO 8601 timestamp of the most recently received heartbeat.
101    pub last_heartbeat_at: Option<String>,
102    /// User-agent string from the last plugin used.
103    pub last_plugin: Option<String>,
104    /// Editor name extracted from the last plugin user-agent.
105    pub last_plugin_name: Option<String>,
106    /// Name of the last project coded in.
107    pub last_project: Option<String>,
108    /// Name of the last branch coded in.
109    pub last_branch: Option<String>,
110    /// Geographic location associated with the account.
111    pub city: Option<City>,
112    /// `GitHub` username.
113    pub github_username: Option<String>,
114    /// Twitter/X handle.
115    pub twitter_username: Option<String>,
116    /// The user's `LinkedIn` username.
117    pub linkedin_username: Option<String>,
118    /// `wonderful.dev` username.
119    pub wonderfuldev_username: Option<String>,
120    // ── Fields below are not listed in the official API docs but are observed
121    // ── in real responses — kept as optional for forward-compatibility.
122    /// Geographic location string (legacy / undocumented).
123    pub location: Option<String>,
124    /// Absolute URL of the user's public profile (undocumented).
125    pub profile_url: Option<String>,
126    /// Whether this account is in write-only mode (undocumented).
127    #[serde(default)]
128    pub writes_only: bool,
129    /// Heartbeat timeout in minutes (undocumented on this endpoint).
130    pub timeout: Option<u32>,
131    /// Whether the user prefers 24-hour time format (undocumented).
132    pub time_format_24hr: Option<bool>,
133    /// ISO 8601 timestamp when the account was created.
134    pub created_at: String,
135    /// ISO 8601 timestamp when the account was last modified.
136    pub modified_at: Option<String>,
137}
138
139// ─────────────────────────────────────────────────────────────────────────────
140// Summaries
141// ─────────────────────────────────────────────────────────────────────────────
142
143/// Top-level envelope returned by `GET /users/current/summaries`.
144#[non_exhaustive]
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SummaryResponse {
147    /// Per-day summary entries (one per calendar day in the requested range).
148    pub data: Vec<SummaryData>,
149    /// ISO 8601 end of the requested range.
150    pub end: String,
151    /// ISO 8601 start of the requested range.
152    pub start: String,
153    /// Cumulative total across all days in the requested range.
154    #[serde(default)]
155    pub cumulative_total: Option<CumulativeTotal>,
156    /// Daily coding average for the requested range.
157    #[serde(default)]
158    pub daily_average: Option<DailyAverage>,
159}
160
161/// Cumulative coding total returned as part of [`SummaryResponse`].
162#[non_exhaustive]
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct CumulativeTotal {
165    /// Total seconds across all days.
166    pub seconds: f64,
167    /// Human-readable total (e.g. `"14 hrs 22 mins"`).
168    pub text: String,
169    /// Total in decimal format (e.g. `"14.38"`).
170    #[serde(default)]
171    pub decimal: String,
172    /// Total in digital clock format (e.g. `"14:22"`).
173    #[serde(default)]
174    pub digital: String,
175}
176
177/// Daily average returned as part of [`SummaryResponse`].
178#[non_exhaustive]
179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180pub struct DailyAverage {
181    /// Number of days in the range with no coding activity logged.
182    #[serde(default)]
183    pub holidays: u32,
184    /// Total number of days in the range.
185    #[serde(default)]
186    pub days_including_holidays: u32,
187    /// Number of days in the range excluding days with no activity.
188    #[serde(default)]
189    pub days_minus_holidays: u32,
190    /// Average seconds per day, excluding the "Other" language.
191    pub seconds: f64,
192    /// Human-readable daily average, excluding the "Other" language.
193    pub text: String,
194    /// Average seconds per day, including all languages.
195    pub seconds_including_other_language: f64,
196    /// Human-readable daily average, including all languages.
197    pub text_including_other_language: String,
198}
199
200/// Coding activity summary for a single calendar day.
201#[non_exhaustive]
202#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203pub struct SummaryData {
204    /// Time broken down by activity category (coding, browsing, etc.).
205    #[serde(default)]
206    pub categories: Vec<SummaryEntry>,
207    /// Time broken down by detected dependency / library.
208    #[serde(default)]
209    pub dependencies: Vec<SummaryEntry>,
210    /// Time broken down by editor.
211    #[serde(default)]
212    pub editors: Vec<SummaryEntry>,
213    /// Daily grand total across all activity.
214    pub grand_total: GrandTotal,
215    /// Time broken down by programming language.
216    #[serde(default)]
217    pub languages: Vec<SummaryEntry>,
218    /// Time broken down by machine / hostname.
219    #[serde(default)]
220    pub machines: Vec<MachineEntry>,
221    /// Time broken down by operating system.
222    #[serde(default)]
223    pub operating_systems: Vec<SummaryEntry>,
224    /// Time broken down by project.
225    #[serde(default)]
226    pub projects: Vec<SummaryEntry>,
227    /// Time broken down by branch — only present when the `project` URL
228    /// parameter is used in the request.
229    #[serde(default)]
230    pub branches: Vec<SummaryEntry>,
231    /// Time broken down by entity (file/domain) — only present when the
232    /// `project` URL parameter is used in the request.
233    #[serde(default)]
234    pub entities: Vec<SummaryEntry>,
235    /// The date range this entry covers.
236    pub range: SummaryRange,
237}
238
239/// A single time-breakdown entry (language, project, editor, OS, etc.).
240#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241pub struct SummaryEntry {
242    /// Human-readable duration in `HH:MM` format.
243    pub digital: String,
244    /// Whole hours component.
245    pub hours: u32,
246    /// Whole minutes component (0–59).
247    pub minutes: u32,
248    /// Entity name (e.g. `"Python"`, `"my-project"`).
249    pub name: String,
250    /// Percentage of total time for the period (0.0–100.0).
251    pub percent: f64,
252    /// Whole seconds component (0–59).
253    ///
254    /// The `WakaTime` API occasionally omits this field (e.g. in `stats/all_time`
255    /// project entries). Defaults to `0` when absent.
256    #[serde(default)]
257    pub seconds: u32,
258    /// Full human-readable duration (e.g. `"3 hrs 30 mins"`).
259    pub text: String,
260    /// Total duration in seconds (fractional).
261    pub total_seconds: f64,
262    /// Lines added by AI coding assistants (present on project entries).
263    #[serde(default)]
264    pub ai_additions: u32,
265    /// Lines removed by AI coding assistants (present on project entries).
266    #[serde(default)]
267    pub ai_deletions: u32,
268    /// Lines added by the developer via keyboard input (present on project entries).
269    #[serde(default)]
270    pub human_additions: u32,
271    /// Lines removed by the developer via keyboard input (present on project entries).
272    #[serde(default)]
273    pub human_deletions: u32,
274}
275
276/// A machine / hostname time-breakdown entry.
277///
278/// This is structurally similar to [`SummaryEntry`] but includes the
279/// `machine_name_id` field returned by the API.
280#[non_exhaustive]
281#[derive(Debug, Clone, Default, Serialize, Deserialize)]
282pub struct MachineEntry {
283    /// Human-readable duration in `HH:MM` format.
284    pub digital: String,
285    /// Whole hours component.
286    pub hours: u32,
287    /// Disambiguated machine identifier returned by the API.
288    pub machine_name_id: String,
289    /// Whole minutes component (0–59).
290    pub minutes: u32,
291    /// Human-readable machine name.
292    pub name: String,
293    /// Percentage of total time for the period (0.0–100.0).
294    pub percent: f64,
295    /// Whole seconds component (0–59).
296    ///
297    /// Defaults to `0` when absent — the API occasionally omits this field.
298    #[serde(default)]
299    pub seconds: u32,
300    /// Full human-readable duration (e.g. `"1 hr 15 mins"`).
301    pub text: String,
302    /// Total duration in seconds (fractional).
303    pub total_seconds: f64,
304}
305
306/// Grand total coding time for a single calendar day.
307#[non_exhaustive]
308#[derive(Debug, Clone, Default, Serialize, Deserialize)]
309pub struct GrandTotal {
310    /// Human-readable duration in `HH:MM` format.
311    pub digital: String,
312    /// Whole hours component.
313    pub hours: u32,
314    /// Whole minutes component (0–59).
315    pub minutes: u32,
316    /// Whole seconds component (0–59).
317    ///
318    /// The `WakaTime` API omits this field from `grand_total` (it is present on
319    /// per-language/project entries). Defaults to `0` when absent.
320    #[serde(default)]
321    pub seconds: u32,
322    /// Full human-readable duration (e.g. `"6 hrs 42 mins"`).
323    pub text: String,
324    /// Total duration in seconds (fractional).
325    pub total_seconds: f64,
326    /// Lines added by AI coding assistants (e.g. GitHub Copilot) this day.
327    #[serde(default)]
328    pub ai_additions: u32,
329    /// Lines removed by AI coding assistants this day.
330    #[serde(default)]
331    pub ai_deletions: u32,
332    /// Lines added by the developer via keyboard input this day.
333    #[serde(default)]
334    pub human_additions: u32,
335    /// Lines removed by the developer via keyboard input this day.
336    #[serde(default)]
337    pub human_deletions: u32,
338}
339
340/// Date range metadata attached to a [`SummaryData`] entry.
341#[non_exhaustive]
342#[derive(Debug, Clone, Default, Serialize, Deserialize)]
343pub struct SummaryRange {
344    /// Calendar date string (e.g. `"2025-01-13"`). Present on single-day
345    /// entries; may be absent on range queries.
346    pub date: Option<String>,
347    /// ISO 8601 end timestamp.
348    pub end: String,
349    /// ISO 8601 start timestamp.
350    pub start: String,
351    /// Human-readable description (e.g. `"today"`, `"yesterday"`).
352    pub text: Option<String>,
353    /// IANA timezone used when computing the range.
354    pub timezone: Option<String>,
355}
356
357// ─────────────────────────────────────────────────────────────────────────────
358// Projects list
359// ─────────────────────────────────────────────────────────────────────────────
360
361/// Top-level envelope returned by `GET /users/current/projects`.
362#[non_exhaustive]
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct ProjectsResponse {
365    /// The list of projects.
366    pub data: Vec<Project>,
367}
368
369/// A `WakaTime` project.
370#[non_exhaustive]
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct Project {
373    /// Optional badge URL associated with the project.
374    pub badge: Option<String>,
375    /// Optional hex color code used in the `WakaTime` web UI.
376    pub color: Option<String>,
377    /// ISO 8601 timestamp when the project was first seen.
378    pub created_at: String,
379    /// Whether the project has a public shareable URL.
380    pub has_public_url: bool,
381    /// Human-readable last-heartbeat description (e.g. `"2 hours ago"`).
382    pub human_readable_last_heartbeat_at: String,
383    /// Human-readable first-heartbeat description.
384    /// Only set for accounts created after 2024-02-05.
385    pub human_readable_first_heartbeat_at: Option<String>,
386    /// Unique project identifier (UUID).
387    pub id: String,
388    /// ISO 8601 timestamp of the last received heartbeat.
389    pub last_heartbeat_at: String,
390    /// ISO 8601 timestamp of the first received heartbeat.
391    /// Only set for accounts created after 2024-02-05.
392    pub first_heartbeat_at: Option<String>,
393    /// Project name.
394    pub name: String,
395    /// Linked repository URL (if configured).
396    pub repository: Option<String>,
397    /// Public project URL (if `has_public_url` is `true`).
398    pub url: Option<String>,
399    /// URL-encoded version of the project name.
400    pub urlencoded_name: String,
401}
402
403// ─────────────────────────────────────────────────────────────────────────────
404// Stats
405// ─────────────────────────────────────────────────────────────────────────────
406
407/// Top-level envelope returned by `GET /users/current/stats/{range}`.
408#[non_exhaustive]
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct StatsResponse {
411    /// The aggregated stats object.
412    pub data: Stats,
413}
414
415/// Aggregated coding stats for a predefined time range.
416#[non_exhaustive]
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct Stats {
419    /// The day with the most coding activity.
420    pub best_day: Option<BestDay>,
421    /// Time broken down by activity category.
422    pub categories: Vec<SummaryEntry>,
423    /// ISO 8601 timestamp when this stats snapshot was created.
424    pub created_at: String,
425    /// Average daily coding time in seconds, excluding "Other" language.
426    pub daily_average: f64,
427    /// Average daily coding time in seconds, including all languages.
428    pub daily_average_including_other_language: f64,
429    /// Number of days in the range including weekends/holidays.
430    pub days_including_holidays: u32,
431    /// Number of working days in the range (excluding days with no activity).
432    pub days_minus_holidays: u32,
433    /// Time broken down by detected dependency / library.
434    #[serde(default)]
435    pub dependencies: Vec<SummaryEntry>,
436    /// Time broken down by editor.
437    pub editors: Vec<SummaryEntry>,
438    /// ISO 8601 end of the stats range.
439    pub end: String,
440    /// Number of holiday / zero-activity days in the range.
441    pub holidays: u32,
442    /// Human-readable average daily coding time, excluding "Other" language.
443    pub human_readable_daily_average: String,
444    /// Human-readable average daily coding time, including all languages.
445    #[serde(default)]
446    pub human_readable_daily_average_including_other_language: String,
447    /// Human-readable description of the range (e.g. `"last 7 days"`).
448    pub human_readable_range: String,
449    /// Human-readable total coding time, excluding "Other" language.
450    pub human_readable_total: String,
451    /// Human-readable total coding time, including all languages.
452    #[serde(default)]
453    pub human_readable_total_including_other_language: String,
454    /// Lines added by AI coding assistants (e.g. GitHub Copilot).
455    #[serde(default)]
456    pub ai_additions: u32,
457    /// Lines removed by AI coding assistants.
458    #[serde(default)]
459    pub ai_deletions: u32,
460    /// Lines added by the developer via keyboard input.
461    #[serde(default)]
462    pub human_additions: u32,
463    /// Lines removed by the developer via keyboard input.
464    #[serde(default)]
465    pub human_deletions: u32,
466    /// Unique stats record identifier (not always present in the API response).
467    pub id: Option<String>,
468    /// Whether the stats snapshot is currently refreshing in the background.
469    #[serde(default)]
470    pub is_already_updating: bool,
471    /// Whether this user's coding activity is publicly visible.
472    #[serde(default)]
473    pub is_coding_activity_visible: bool,
474    /// Whether this user's language usage is publicly visible.
475    #[serde(default)]
476    pub is_language_usage_visible: bool,
477    /// Whether this user's editor usage is publicly visible.
478    #[serde(default)]
479    pub is_editor_usage_visible: bool,
480    /// Whether this user's category usage is publicly visible.
481    #[serde(default)]
482    pub is_category_usage_visible: bool,
483    /// Whether this user's operating system usage is publicly visible.
484    #[serde(default)]
485    pub is_os_usage_visible: bool,
486    /// Whether the stats calculation got stuck and will be retried.
487    #[serde(default)]
488    pub is_stuck: bool,
489    /// Whether these stats include the current (partial) day.
490    #[serde(default)]
491    pub is_including_today: bool,
492    /// Whether the stats snapshot is up to date.
493    pub is_up_to_date: bool,
494    /// Time broken down by programming language.
495    pub languages: Vec<SummaryEntry>,
496    /// Time broken down by machine.
497    pub machines: Vec<MachineEntry>,
498    /// ISO 8601 timestamp when this snapshot was last modified.
499    pub modified_at: Option<String>,
500    /// Time broken down by operating system.
501    pub operating_systems: Vec<SummaryEntry>,
502    /// How fully computed the stats are (0–100).
503    pub percent_calculated: u32,
504    /// Time broken down by project.
505    pub projects: Vec<SummaryEntry>,
506    /// Named range (e.g. `"last_7_days"`, `"last_30_days"`).
507    pub range: String,
508    /// ISO 8601 start of the stats range.
509    pub start: String,
510    /// Computation status (e.g. `"ok"`, `"pending_update"`).
511    pub status: String,
512    /// Heartbeat timeout in minutes used for this calculation.
513    pub timeout: u32,
514    /// IANA timezone used when computing the stats.
515    pub timezone: String,
516    /// Total coding time in seconds, excluding "Other" language.
517    pub total_seconds: f64,
518    /// Total coding time in seconds, including all languages.
519    pub total_seconds_including_other_language: f64,
520    /// Owner user identifier (UUID).
521    pub user_id: String,
522    /// Owner username.
523    pub username: String,
524    /// Whether the account is in write-only mode.
525    pub writes_only: bool,
526}
527
528/// The single best (most productive) day in a stats range.
529#[non_exhaustive]
530#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct BestDay {
532    /// Calendar date (e.g. `"2025-01-13"`).
533    pub date: String,
534    /// Human-readable total for that day.
535    pub text: String,
536    /// Total coding seconds for that day.
537    pub total_seconds: f64,
538}
539
540// ─────────────────────────────────────────────────────────────────────────────
541// Goals
542// ─────────────────────────────────────────────────────────────────────────────
543
544/// Top-level envelope returned by `GET /users/current/goals`.
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct GoalsResponse {
547    /// The list of goals.
548    pub data: Vec<Goal>,
549    /// Total number of goals.
550    pub total: u32,
551    /// Total number of pages.
552    pub total_pages: u32,
553}
554
555/// Minimal user info embedded in a [`Goal`] (owner / shared-with).
556#[non_exhaustive]
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct GoalOwner {
559    /// Human-readable display name.
560    pub display_name: Option<String>,
561    /// Email address.
562    pub email: Option<String>,
563    /// Full legal name.
564    pub full_name: Option<String>,
565    /// Unique user identifier (UUID).
566    pub id: String,
567    /// Avatar URL.
568    pub photo: Option<String>,
569    /// Public username.
570    pub username: Option<String>,
571}
572
573/// A user this goal has been shared with.
574#[non_exhaustive]
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct GoalSharedWith {
577    /// Human-readable display name.
578    pub display_name: Option<String>,
579    /// Email address (only set if shared via email).
580    pub email: Option<String>,
581    /// Full legal name.
582    pub full_name: Option<String>,
583    /// Unique user identifier (UUID).
584    pub id: Option<String>,
585    /// Avatar URL.
586    pub photo: Option<String>,
587    /// Invitation acceptance status.
588    pub status: Option<String>,
589    /// User ID (only set if shared via user id).
590    pub user_id: Option<String>,
591    /// Username (only set if shared via username).
592    pub username: Option<String>,
593}
594
595/// A subscriber to a goal's progress emails.
596#[non_exhaustive]
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct GoalSubscriber {
599    /// Subscriber's public email (if available).
600    pub email: Option<String>,
601    /// How often this subscriber receives emails.
602    pub email_frequency: Option<String>,
603    /// Subscriber's full name (if available).
604    pub full_name: Option<String>,
605    /// Unique user identifier (UUID).
606    pub user_id: String,
607    /// Subscriber's username (if defined).
608    pub username: Option<String>,
609}
610
611/// A single coding goal.
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct Goal {
614    /// `"fail"` when failure days outnumber success days, otherwise `"success"`.
615    pub average_status: Option<String>,
616    /// Per-period chart data (populated when fetching goal details).
617    #[serde(default)]
618    pub chart_data: Option<Vec<GoalChartEntry>>,
619    /// ISO 8601 timestamp when the goal was created.
620    pub created_at: String,
621    /// Overall cumulative status across all delta periods
622    /// (`"success"`, `"fail"`, or `"ignored"`).
623    pub cumulative_status: Option<String>,
624    /// Optional user-defined title that overrides the generated title.
625    pub custom_title: Option<String>,
626    /// Goal period (e.g. `"day"`, `"week"`).
627    pub delta: String,
628    /// Editors this goal is restricted to (empty = all editors).
629    #[serde(default)]
630    pub editors: Vec<String>,
631    /// Unique goal identifier (UUID).
632    pub id: String,
633    /// Days of the week to ignore (e.g. `["saturday", "sunday"]`).
634    #[serde(default)]
635    pub ignore_days: Vec<String>,
636    /// Whether days with zero activity are excluded from streak calculations.
637    pub ignore_zero_days: bool,
638    /// Target improvement percentage over baseline.
639    pub improve_by_percent: Option<f64>,
640    /// Whether the currently authenticated user owns this goal.
641    #[serde(default)]
642    pub is_current_user_owner: bool,
643    /// Whether the goal is active.
644    pub is_enabled: bool,
645    /// Whether passing means staying *below* the target.
646    pub is_inverse: bool,
647    /// Whether the goal is temporarily snoozed.
648    pub is_snoozed: bool,
649    /// Whether achievements are tweeted automatically.
650    pub is_tweeting: bool,
651    /// Languages this goal is restricted to (empty = all languages).
652    #[serde(default)]
653    pub languages: Vec<String>,
654    /// ISO 8601 timestamp when the goal was last modified (optional).
655    pub modified_at: Option<String>,
656    /// Owner of this goal.
657    pub owner: Option<GoalOwner>,
658    /// Projects this goal is restricted to (empty = all projects).
659    #[serde(default)]
660    pub projects: Vec<String>,
661    /// Human-readable range description covering all delta periods.
662    pub range_text: Option<String>,
663    /// Status for the most recent period (`"success"`, `"fail"`, etc.).
664    /// Present at the top level on list responses.
665    pub range_status: Option<String>,
666    /// Human-readable explanation of the most recent period's status.
667    pub range_status_reason: Option<String>,
668    /// Target coding seconds per period.
669    pub seconds: f64,
670    /// Users this goal has been shared with.
671    #[serde(default)]
672    pub shared_with: Vec<GoalSharedWith>,
673    /// ISO 8601 timestamp when email notifications will be re-enabled.
674    pub snooze_until: Option<String>,
675    /// Overall goal status (e.g. `"success"`, `"fail"`, `"ignored"`, `"pending"`).
676    pub status: String,
677    /// Percent calculated (0–100) for goals that are pre-calculated in the background.
678    #[serde(default)]
679    pub status_percent_calculated: u32,
680    /// Goal subscribers (users who receive progress emails).
681    #[serde(default)]
682    pub subscribers: Vec<GoalSubscriber>,
683    /// Human-readable goal title.
684    pub title: String,
685    /// Goal type (e.g. `"coding"`).
686    #[serde(rename = "type")]
687    pub goal_type: String,
688}
689
690/// One data point in a goal's progress chart.
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct GoalChartEntry {
693    /// Actual coding seconds logged during this period.
694    pub actual_seconds: f64,
695    /// Human-readable actual coding time.
696    pub actual_seconds_text: String,
697    /// Target coding seconds for this period.
698    pub goal_seconds: f64,
699    /// Human-readable target coding time.
700    pub goal_seconds_text: String,
701    /// Date range this data point covers.
702    pub range: GoalChartRange,
703    /// Status for this period (e.g. `"success"`, `"fail"`).
704    pub range_status: String,
705    /// Human-readable explanation of the status.
706    pub range_status_reason: String,
707}
708
709/// Date range metadata for a [`GoalChartEntry`].
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct GoalChartRange {
712    /// Calendar date (e.g. `"2025-01-13"`). Only present when `delta` is `"day"`.
713    pub date: Option<String>,
714    /// ISO 8601 end timestamp.
715    pub end: String,
716    /// ISO 8601 start timestamp.
717    pub start: String,
718    /// Human-readable description (e.g. `"Mon, Jan 13"`).
719    pub text: String,
720    /// IANA timezone used when computing the range.
721    pub timezone: String,
722}
723
724// ─────────────────────────────────────────────────────────────────────────────
725// Leaderboard
726// ─────────────────────────────────────────────────────────────────────────────
727
728/// Top-level envelope returned by `GET /users/current/leaderboards`.
729#[non_exhaustive]
730#[derive(Debug, Clone, Serialize, Deserialize)]
731pub struct LeaderboardResponse {
732    /// The current authenticated user's entry, if they appear in this page.
733    pub current_user: Option<LeaderboardEntry>,
734    /// The leaderboard entries for this page.
735    pub data: Vec<LeaderboardEntry>,
736    /// Language filter applied (if any).
737    pub language: Option<String>,
738    /// ISO 8601 timestamp when the leaderboard was last updated.
739    /// The `WakaTime` API occasionally omits this field.
740    pub modified_at: Option<String>,
741    /// Current page number (1-based).
742    #[serde(default)]
743    pub page: u32,
744    /// Date range this leaderboard covers.
745    pub range: Option<LeaderboardRange>,
746    /// Heartbeat timeout in minutes used for ranking.
747    #[serde(default)]
748    pub timeout: u32,
749    /// Total number of pages available.
750    #[serde(default)]
751    pub total_pages: u32,
752    /// Whether the leaderboard is restricted to write-only accounts.
753    #[serde(default)]
754    pub writes_only: bool,
755}
756
757/// A single entry on the leaderboard.
758///
759/// Used for both regular entries in `data` and the `current_user` object.
760/// `rank` is `None` when the authenticated user is not on the current page.
761#[non_exhaustive]
762#[derive(Debug, Clone, Serialize, Deserialize)]
763pub struct LeaderboardEntry {
764    /// The rank of this user (1 = top coder).
765    /// `None` when the `current_user` is not present on this page.
766    pub rank: Option<u32>,
767    /// Aggregated coding totals for this user.
768    pub running_total: RunningTotal,
769    /// Public user information.
770    pub user: LeaderboardUser,
771}
772
773/// Aggregated coding totals used on the leaderboard.
774#[non_exhaustive]
775#[derive(Debug, Clone, Serialize, Deserialize)]
776pub struct RunningTotal {
777    /// Average daily coding time in seconds.
778    pub daily_average: f64,
779    /// Human-readable average daily coding time.
780    pub human_readable_daily_average: String,
781    /// Human-readable total coding time for the period.
782    pub human_readable_total: String,
783    /// Top languages for this user (may be empty if privacy settings prevent
784    /// disclosure).
785    #[serde(default)]
786    pub languages: Vec<SummaryEntry>,
787    /// ISO 8601 timestamp when this total was last computed (not always present).
788    pub modified_at: Option<String>,
789    /// Total coding seconds for the leaderboard period.
790    pub total_seconds: f64,
791}
792
793/// Public user information exposed on the leaderboard.
794#[non_exhaustive]
795#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct LeaderboardUser {
797    /// Human-readable display name.
798    pub display_name: String,
799    /// Public email (empty string if not shared).
800    pub email: Option<String>,
801    /// Full legal name.
802    pub full_name: Option<String>,
803    /// Human-readable website URL.
804    pub human_readable_website: Option<String>,
805    /// Unique user identifier (UUID).
806    pub id: String,
807    /// Whether the email address is publicly visible.
808    pub is_email_public: bool,
809    /// Whether the user is open to work.
810    pub is_hireable: bool,
811    /// Geographic location string.
812    pub location: Option<String>,
813    /// Avatar image URL.
814    pub photo: Option<String>,
815    /// Whether the avatar is publicly visible.
816    pub photo_public: bool,
817    /// Absolute URL of the user's public profile.
818    pub profile_url: Option<String>,
819    /// Public email address (distinct from account email).
820    pub public_email: Option<String>,
821    /// Login handle.
822    pub username: Option<String>,
823    /// Personal website URL.
824    pub website: Option<String>,
825}
826
827/// Date range metadata for a [`LeaderboardResponse`].
828#[non_exhaustive]
829#[derive(Debug, Clone, Serialize, Deserialize)]
830pub struct LeaderboardRange {
831    /// End date string (e.g. `"2025-01-13"`).
832    pub end_date: String,
833    /// Short human-readable end date (e.g. `"Jan 13"`).
834    pub end_text: String,
835    /// Named range identifier (e.g. `"last_7_days"`).
836    pub name: String,
837    /// Start date string (e.g. `"2025-01-07"`).
838    pub start_date: String,
839    /// Short human-readable start date (e.g. `"Jan 7"`).
840    pub start_text: String,
841    /// Full human-readable range description (e.g. `"Last 7 Days"`).
842    pub text: String,
843}
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848
849    /// Deserializing a minimal [`GrandTotal`] from JSON must succeed and
850    /// `total_seconds` must be preserved exactly.
851    #[test]
852    fn grand_total_deserialize() {
853        let json = r#"{
854            "digital": "6:42",
855            "hours": 6,
856            "minutes": 42,
857            "seconds": 0,
858            "text": "6 hrs 42 mins",
859            "total_seconds": 24120.0
860        }"#;
861        let gt: GrandTotal = serde_json::from_str(json).unwrap();
862        assert_eq!(gt.hours, 6);
863        assert_eq!(gt.minutes, 42);
864        #[allow(clippy::float_cmp)]
865        {
866            assert_eq!(gt.total_seconds, 24120.0);
867        }
868        assert_eq!(gt.text, "6 hrs 42 mins");
869    }
870
871    /// Unknown fields in the JSON must be silently ignored.
872    #[test]
873    fn summary_entry_ignores_unknown_fields() {
874        let json = r#"{
875            "digital": "3:30",
876            "hours": 3,
877            "minutes": 30,
878            "name": "Python",
879            "percent": 52.3,
880            "seconds": 0,
881            "text": "3 hrs 30 mins",
882            "total_seconds": 12600.0,
883            "some_future_field": "ignored"
884        }"#;
885        let entry: SummaryEntry = serde_json::from_str(json).unwrap();
886        assert_eq!(entry.name, "Python");
887        assert_eq!(entry.hours, 3);
888    }
889
890    /// `SummaryData::dependencies` must default to an empty vec when absent.
891    #[test]
892    fn summary_data_default_dependencies() {
893        let json = r#"{
894            "categories": [],
895            "editors": [],
896            "grand_total": {
897                "digital": "0:00",
898                "hours": 0,
899                "minutes": 0,
900                "seconds": 0,
901                "text": "0 secs",
902                "total_seconds": 0.0
903            },
904            "languages": [],
905            "operating_systems": [],
906            "projects": [],
907            "range": {
908                "end": "2025-01-14T00:00:00Z",
909                "start": "2025-01-13T00:00:00Z"
910            }
911        }"#;
912        let data: SummaryData = serde_json::from_str(json).unwrap();
913        assert!(data.dependencies.is_empty());
914        assert!(data.machines.is_empty());
915    }
916
917    /// A serialized `User` must round-trip through JSON without loss.
918    #[test]
919    fn user_roundtrip() {
920        let user = User {
921            id: "abc-123".to_owned(),
922            username: "johndoe".to_owned(),
923            display_name: "John Doe".to_owned(),
924            full_name: Some("John Doe".to_owned()),
925            bio: None,
926            email: None,
927            public_email: None,
928            photo: None,
929            timezone: "UTC".to_owned(),
930            website: None,
931            human_readable_website: None,
932            plan: Some("free".to_owned()),
933            has_premium_features: false,
934            is_email_public: false,
935            is_photo_public: false,
936            is_email_confirmed: true,
937            is_hireable: false,
938            logged_time_public: false,
939            languages_used_public: false,
940            editors_used_public: false,
941            categories_used_public: false,
942            os_used_public: false,
943            last_heartbeat_at: None,
944            last_plugin: None,
945            last_plugin_name: None,
946            last_project: None,
947            last_branch: None,
948            city: None,
949            github_username: None,
950            twitter_username: None,
951            linkedin_username: None,
952            wonderfuldev_username: None,
953            location: None,
954            profile_url: Some("https://wakatime.com/@johndoe".to_owned()),
955            writes_only: false,
956            timeout: Some(15),
957            time_format_24hr: Some(false),
958            created_at: "2024-01-01T00:00:00Z".to_owned(),
959            modified_at: None,
960        };
961        let json = serde_json::to_string(&user).unwrap();
962        let user2: User = serde_json::from_str(&json).unwrap();
963        assert_eq!(user.id, user2.id);
964        assert_eq!(user.username, user2.username);
965        assert_eq!(user.timezone, user2.timezone);
966    }
967}