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(['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), 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); 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), 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}