1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct ActivitySummary {
11 pub activity_id: u64,
13
14 #[serde(default)]
16 pub activity_name: Option<String>,
17
18 #[serde(default)]
20 pub start_time_local: Option<String>,
21
22 #[serde(default)]
24 pub start_time_gmt: Option<String>,
25
26 #[serde(default)]
28 pub activity_type: Option<ActivityType>,
29
30 #[serde(default)]
32 pub distance: Option<f64>,
33
34 #[serde(default)]
36 pub duration: Option<f64>,
37
38 #[serde(default)]
40 pub elapsed_duration: Option<f64>,
41
42 #[serde(default)]
44 pub moving_duration: Option<f64>,
45
46 #[serde(default)]
48 pub calories: Option<f64>,
49
50 #[serde(default)]
52 pub average_hr: Option<f64>,
53
54 #[serde(default)]
56 pub max_hr: Option<f64>,
57
58 #[serde(default)]
60 pub average_speed: Option<f64>,
61
62 #[serde(default)]
64 pub max_speed: Option<f64>,
65
66 #[serde(default)]
68 pub elevation_gain: Option<f64>,
69
70 #[serde(default)]
72 pub elevation_loss: Option<f64>,
73
74 #[serde(default)]
76 pub average_running_cadence_in_steps_per_minute: Option<f64>,
77
78 #[serde(default)]
80 pub steps: Option<u64>,
81
82 #[serde(default)]
84 pub has_polyline: Option<bool>,
85
86 #[serde(default)]
88 pub owner_display_name: Option<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ActivityType {
95 pub type_key: String,
97
98 #[serde(default)]
100 pub type_id: Option<u64>,
101
102 #[serde(default)]
104 pub parent_type_id: Option<u64>,
105
106 #[serde(default)]
108 pub is_hidden: Option<bool>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct ActivityDetails {
115 pub activity_id: u64,
117
118 #[serde(default)]
120 pub activity_name: Option<String>,
121
122 #[serde(default)]
124 pub description: Option<String>,
125
126 #[serde(default)]
128 pub start_time_local: Option<String>,
129
130 #[serde(default)]
132 pub start_time_gmt: Option<String>,
133
134 #[serde(default)]
136 pub activity_type: Option<ActivityType>,
137
138 #[serde(default)]
140 pub summary_dto: Option<serde_json::Value>,
141
142 #[serde(default)]
144 pub location_name: Option<String>,
145
146 #[serde(default)]
148 pub time_zone_unit_dto: Option<serde_json::Value>,
149
150 #[serde(default)]
152 pub metadata_dto: Option<serde_json::Value>,
153
154 #[serde(flatten)]
156 pub extra: serde_json::Map<String, serde_json::Value>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct UploadResult {
163 pub detailed_import_result: DetailedImportResult,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct DetailedImportResult {
171 pub upload_id: u64,
173
174 #[serde(default)]
176 pub upload_uuid: Option<UploadUuid>,
177
178 #[serde(default)]
180 pub owner: Option<u64>,
181
182 #[serde(default)]
184 pub file_size: Option<u64>,
185
186 #[serde(default)]
188 pub processing_time: Option<u64>,
189
190 #[serde(default)]
192 pub creation_date: Option<String>,
193
194 #[serde(default)]
196 pub file_name: Option<String>,
197
198 #[serde(default)]
200 pub successes: Vec<UploadSuccess>,
201
202 #[serde(default)]
204 pub failures: Vec<serde_json::Value>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct UploadUuid {
210 pub uuid: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct UploadSuccess {
217 #[serde(default)]
219 pub internal_id: Option<u64>,
220
221 #[serde(default)]
223 pub external_id: Option<String>,
224
225 #[serde(default)]
227 pub messages: Vec<serde_json::Value>,
228}
229
230impl ActivitySummary {
231 pub fn display_name(&self) -> String {
233 self.activity_name
234 .clone()
235 .unwrap_or_else(|| "Unnamed Activity".to_string())
236 }
237
238 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 pub fn distance_km(&self) -> Option<f64> {
248 self.distance.map(|d| d / 1000.0)
249 }
250
251 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 pub fn date(&self) -> String {
271 self.start_time_local
272 .as_ref()
273 .map(|s| {
274 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), 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); 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), 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}