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(|c| c == 'T' || c == ' ')
276                    .next()
277                    .unwrap_or(s)
278                    .to_string()
279            })
280            .unwrap_or_else(|| "-".to_string())
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_activity_summary_display_name() {
290        let activity = ActivitySummary {
291            activity_id: 123,
292            activity_name: Some("Morning Run".to_string()),
293            start_time_local: None,
294            start_time_gmt: None,
295            activity_type: None,
296            distance: None,
297            duration: None,
298            elapsed_duration: None,
299            moving_duration: None,
300            calories: None,
301            average_hr: None,
302            max_hr: None,
303            average_speed: None,
304            max_speed: None,
305            elevation_gain: None,
306            elevation_loss: None,
307            average_running_cadence_in_steps_per_minute: None,
308            steps: None,
309            has_polyline: None,
310            owner_display_name: None,
311        };
312        assert_eq!(activity.display_name(), "Morning Run");
313    }
314
315    #[test]
316    fn test_activity_summary_duration_formatted() {
317        let mut activity = ActivitySummary {
318            activity_id: 123,
319            activity_name: None,
320            start_time_local: None,
321            start_time_gmt: None,
322            activity_type: None,
323            distance: None,
324            duration: Some(3661.0), // 1h 1m 1s
325            elapsed_duration: None,
326            moving_duration: None,
327            calories: None,
328            average_hr: None,
329            max_hr: None,
330            average_speed: None,
331            max_speed: None,
332            elevation_gain: None,
333            elevation_loss: None,
334            average_running_cadence_in_steps_per_minute: None,
335            steps: None,
336            has_polyline: None,
337            owner_display_name: None,
338        };
339        assert_eq!(activity.duration_formatted(), "1:01:01");
340
341        activity.duration = Some(125.0); // 2m 5s
342        assert_eq!(activity.duration_formatted(), "2:05");
343    }
344
345    #[test]
346    fn test_activity_summary_distance_km() {
347        let activity = ActivitySummary {
348            activity_id: 123,
349            activity_name: None,
350            start_time_local: None,
351            start_time_gmt: None,
352            activity_type: None,
353            distance: Some(10500.0), // 10.5 km
354            duration: None,
355            elapsed_duration: None,
356            moving_duration: None,
357            calories: None,
358            average_hr: None,
359            max_hr: None,
360            average_speed: None,
361            max_speed: None,
362            elevation_gain: None,
363            elevation_loss: None,
364            average_running_cadence_in_steps_per_minute: None,
365            steps: None,
366            has_polyline: None,
367            owner_display_name: None,
368        };
369        assert_eq!(activity.distance_km(), Some(10.5));
370    }
371}