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}