Skip to main content

garmin_cli/models/
activity.rs

1//! Activity data models for Garmin Connect API
2//!
3//! These structures represent activities returned from the Garmin Connect API.
4
5use serde::{Deserialize, Serialize};
6
7/// Activity summary returned from the activity list endpoint
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct ActivitySummary {
11    /// Unique activity identifier
12    pub activity_id: u64,
13
14    /// User-provided or auto-generated activity name
15    #[serde(default)]
16    pub activity_name: Option<String>,
17
18    /// Start time in local timezone (ISO 8601 format)
19    #[serde(default)]
20    pub start_time_local: Option<String>,
21
22    /// Start time in GMT (ISO 8601 format)
23    #[serde(default)]
24    pub start_time_gmt: Option<String>,
25
26    /// Activity type information
27    #[serde(default)]
28    pub activity_type: Option<ActivityType>,
29
30    /// Distance in meters
31    #[serde(default)]
32    pub distance: Option<f64>,
33
34    /// Duration in seconds
35    #[serde(default)]
36    pub duration: Option<f64>,
37
38    /// Elapsed duration in seconds (including pauses)
39    #[serde(default)]
40    pub elapsed_duration: Option<f64>,
41
42    /// Moving duration in seconds
43    #[serde(default)]
44    pub moving_duration: Option<f64>,
45
46    /// Calories burned
47    #[serde(default)]
48    pub calories: Option<f64>,
49
50    /// Average heart rate in bpm
51    #[serde(default)]
52    pub average_hr: Option<f64>,
53
54    /// Maximum heart rate in bpm
55    #[serde(default)]
56    pub max_hr: Option<f64>,
57
58    /// Average speed in m/s
59    #[serde(default)]
60    pub average_speed: Option<f64>,
61
62    /// Maximum speed in m/s
63    #[serde(default)]
64    pub max_speed: Option<f64>,
65
66    /// Total elevation gain in meters
67    #[serde(default)]
68    pub elevation_gain: Option<f64>,
69
70    /// Total elevation loss in meters
71    #[serde(default)]
72    pub elevation_loss: Option<f64>,
73
74    /// Average running cadence in steps per minute
75    #[serde(default)]
76    pub average_running_cadence_in_steps_per_minute: Option<f64>,
77
78    /// Steps count
79    #[serde(default)]
80    pub steps: Option<u64>,
81
82    /// Whether the activity has GPS data
83    #[serde(default)]
84    pub has_polyline: Option<bool>,
85
86    /// Owner display name
87    #[serde(default)]
88    pub owner_display_name: Option<String>,
89}
90
91/// Activity type information
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ActivityType {
95    /// Type key (e.g., "running", "cycling", "walking")
96    pub type_key: String,
97
98    /// Type ID
99    #[serde(default)]
100    pub type_id: Option<u64>,
101
102    /// Parent type key
103    #[serde(default)]
104    pub parent_type_id: Option<u64>,
105
106    /// Whether this is a custom activity type
107    #[serde(default)]
108    pub is_hidden: Option<bool>,
109}
110
111/// Full activity details (more comprehensive than summary)
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct ActivityDetails {
115    /// Unique activity identifier
116    pub activity_id: u64,
117
118    /// Activity name
119    #[serde(default)]
120    pub activity_name: Option<String>,
121
122    /// Activity description
123    #[serde(default)]
124    pub description: Option<String>,
125
126    /// Start time in local timezone
127    #[serde(default)]
128    pub start_time_local: Option<String>,
129
130    /// Start time in GMT
131    #[serde(default)]
132    pub start_time_gmt: Option<String>,
133
134    /// Activity type
135    #[serde(default)]
136    pub activity_type: Option<ActivityType>,
137
138    /// Summary data (contains metrics like distance, duration, etc.)
139    #[serde(default)]
140    pub summary_dto: Option<serde_json::Value>,
141
142    /// Location name
143    #[serde(default)]
144    pub location_name: Option<String>,
145
146    /// Time zone unit
147    #[serde(default)]
148    pub time_zone_unit_dto: Option<serde_json::Value>,
149
150    /// Metadata
151    #[serde(default)]
152    pub metadata_dto: Option<serde_json::Value>,
153
154    /// Catch-all for unknown fields
155    #[serde(flatten)]
156    pub extra: serde_json::Map<String, serde_json::Value>,
157}
158
159/// Response from activity upload
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct UploadResult {
163    /// Detailed import result
164    pub detailed_import_result: DetailedImportResult,
165}
166
167/// Detailed import result from upload
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct DetailedImportResult {
171    /// Upload ID
172    pub upload_id: u64,
173
174    /// Upload UUID
175    #[serde(default)]
176    pub upload_uuid: Option<UploadUuid>,
177
178    /// Owner ID
179    #[serde(default)]
180    pub owner: Option<u64>,
181
182    /// File size in bytes
183    #[serde(default)]
184    pub file_size: Option<u64>,
185
186    /// Processing time in milliseconds
187    #[serde(default)]
188    pub processing_time: Option<u64>,
189
190    /// Creation date
191    #[serde(default)]
192    pub creation_date: Option<String>,
193
194    /// Original file name
195    #[serde(default)]
196    pub file_name: Option<String>,
197
198    /// Successful imports
199    #[serde(default)]
200    pub successes: Vec<UploadSuccess>,
201
202    /// Failed imports
203    #[serde(default)]
204    pub failures: Vec<serde_json::Value>,
205}
206
207/// Upload UUID
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct UploadUuid {
210    pub uuid: String,
211}
212
213/// Successful upload entry
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct UploadSuccess {
217    /// Created activity ID
218    #[serde(default)]
219    pub internal_id: Option<u64>,
220
221    /// External ID
222    #[serde(default)]
223    pub external_id: Option<String>,
224
225    /// Messages
226    #[serde(default)]
227    pub messages: Vec<serde_json::Value>,
228}
229
230impl ActivitySummary {
231    /// Get a display-friendly name for the activity
232    pub fn display_name(&self) -> String {
233        self.activity_name
234            .clone()
235            .unwrap_or_else(|| "Unnamed Activity".to_string())
236    }
237
238    /// Get the activity type key
239    pub fn type_key(&self) -> String {
240        self.activity_type
241            .as_ref()
242            .map(|t| t.type_key.clone())
243            .unwrap_or_else(|| "unknown".to_string())
244    }
245
246    /// Get distance in kilometers
247    pub fn distance_km(&self) -> Option<f64> {
248        self.distance.map(|d| d / 1000.0)
249    }
250
251    /// Get duration formatted as HH:MM:SS
252    pub fn duration_formatted(&self) -> String {
253        match self.duration {
254            Some(secs) => {
255                let total_secs = secs as u64;
256                let hours = total_secs / 3600;
257                let minutes = (total_secs % 3600) / 60;
258                let seconds = total_secs % 60;
259                if hours > 0 {
260                    format!("{}:{:02}:{:02}", hours, minutes, seconds)
261                } else {
262                    format!("{}:{:02}", minutes, seconds)
263                }
264            }
265            None => "-".to_string(),
266        }
267    }
268
269    /// Get the date portion of start time
270    pub fn date(&self) -> String {
271        self.start_time_local
272            .as_ref()
273            .map(|s| {
274                // Handle both ISO format (T separator) and space-separated format
275                s.split(['T', ' ']).next().unwrap_or(s).to_string()
276            })
277            .unwrap_or_else(|| "-".to_string())
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_activity_summary_display_name() {
287        let activity = ActivitySummary {
288            activity_id: 123,
289            activity_name: Some("Morning Run".to_string()),
290            start_time_local: None,
291            start_time_gmt: None,
292            activity_type: None,
293            distance: None,
294            duration: None,
295            elapsed_duration: None,
296            moving_duration: None,
297            calories: None,
298            average_hr: None,
299            max_hr: None,
300            average_speed: None,
301            max_speed: None,
302            elevation_gain: None,
303            elevation_loss: None,
304            average_running_cadence_in_steps_per_minute: None,
305            steps: None,
306            has_polyline: None,
307            owner_display_name: None,
308        };
309        assert_eq!(activity.display_name(), "Morning Run");
310    }
311
312    #[test]
313    fn test_activity_summary_duration_formatted() {
314        let mut activity = ActivitySummary {
315            activity_id: 123,
316            activity_name: None,
317            start_time_local: None,
318            start_time_gmt: None,
319            activity_type: None,
320            distance: None,
321            duration: Some(3661.0), // 1h 1m 1s
322            elapsed_duration: None,
323            moving_duration: None,
324            calories: None,
325            average_hr: None,
326            max_hr: None,
327            average_speed: None,
328            max_speed: None,
329            elevation_gain: None,
330            elevation_loss: None,
331            average_running_cadence_in_steps_per_minute: None,
332            steps: None,
333            has_polyline: None,
334            owner_display_name: None,
335        };
336        assert_eq!(activity.duration_formatted(), "1:01:01");
337
338        activity.duration = Some(125.0); // 2m 5s
339        assert_eq!(activity.duration_formatted(), "2:05");
340    }
341
342    #[test]
343    fn test_activity_summary_distance_km() {
344        let activity = ActivitySummary {
345            activity_id: 123,
346            activity_name: None,
347            start_time_local: None,
348            start_time_gmt: None,
349            activity_type: None,
350            distance: Some(10500.0), // 10.5 km
351            duration: None,
352            elapsed_duration: None,
353            moving_duration: None,
354            calories: None,
355            average_hr: None,
356            max_hr: None,
357            average_speed: None,
358            max_speed: None,
359            elevation_gain: None,
360            elevation_loss: None,
361            average_running_cadence_in_steps_per_minute: None,
362            steps: None,
363            has_polyline: None,
364            owner_display_name: None,
365        };
366        assert_eq!(activity.distance_km(), Some(10.5));
367    }
368}