1use crate::{PaginatedResponse, Photo, Polyline, Result, RideWithGpsClient, Visibility};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct TripTrackPoint {
9 pub x: Option<f64>,
11
12 pub y: Option<f64>,
14
15 pub d: Option<f64>,
17
18 pub e: Option<f64>,
20
21 pub t: Option<i64>,
23
24 pub s: Option<f64>,
26
27 #[serde(rename = "T")]
29 pub temp: Option<f64>,
30
31 pub h: Option<f64>,
33
34 pub c: Option<f64>,
36
37 pub p: Option<f64>,
39
40 pub pb: Option<f64>,
42
43 pub lap: Option<bool>,
45
46 pub k: Option<bool>,
48
49 pub m: Option<bool>,
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize)]
55pub struct Gear {
56 pub id: u64,
58
59 pub make: Option<String>,
61
62 pub model: Option<String>,
64
65 pub description: Option<String>,
67
68 pub exclude_from_totals: Option<bool>,
70
71 pub created_at: Option<String>,
73}
74
75#[derive(Debug, Clone, Deserialize, Serialize)]
77pub struct Trip {
78 pub id: u64,
80
81 pub name: Option<String>,
83
84 pub description: Option<String>,
86
87 pub distance: Option<f64>,
89
90 pub elevation_gain: Option<f64>,
92
93 pub elevation_loss: Option<f64>,
95
96 pub visibility: Option<Visibility>,
98
99 pub user_id: Option<u64>,
101
102 pub url: Option<String>,
104
105 pub web_url: Option<String>,
107
108 pub departed_at: Option<String>,
110
111 pub time_zone: Option<String>,
113
114 pub created_at: Option<String>,
116
117 pub updated_at: Option<String>,
119
120 pub duration: Option<f64>,
122
123 pub moving_time: Option<f64>,
125
126 pub avg_speed: Option<f64>,
128
129 pub max_speed: Option<f64>,
131
132 pub avg_cad: Option<f64>,
134
135 pub min_cad: Option<f64>,
137
138 pub max_cad: Option<f64>,
140
141 pub avg_hr: Option<f64>,
143
144 pub min_hr: Option<f64>,
146
147 pub max_hr: Option<f64>,
149
150 pub avg_watts: Option<f64>,
152
153 pub min_watts: Option<f64>,
155
156 pub max_watts: Option<f64>,
158
159 pub calories: Option<f64>,
161
162 pub device: Option<String>,
164
165 pub locality: Option<String>,
167
168 pub administrative_area: Option<String>,
170
171 pub country_code: Option<String>,
173
174 pub activity_type: Option<String>,
176
177 pub fit_sport: Option<i32>,
179
180 pub fit_sub_sport: Option<i32>,
182
183 pub stationary: Option<bool>,
185
186 pub track_type: Option<String>,
188
189 pub terrain: Option<i32>,
191
192 pub difficulty: Option<i32>,
194
195 pub first_lat: Option<f64>,
197
198 pub first_lng: Option<f64>,
200
201 pub last_lat: Option<f64>,
203
204 pub last_lng: Option<f64>,
206
207 pub sw_lat: Option<f64>,
209
210 pub sw_lng: Option<f64>,
212
213 pub ne_lat: Option<f64>,
215
216 pub ne_lng: Option<f64>,
218
219 pub track_points: Option<Vec<TripTrackPoint>>,
221
222 pub gear: Option<Gear>,
224
225 pub photos: Option<Vec<Photo>>,
227}
228
229#[derive(Debug, Clone, Default, Serialize)]
231pub struct ListTripsParams {
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub name: Option<String>,
235
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub visibility: Option<Visibility>,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub min_distance: Option<f64>,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub max_distance: Option<f64>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub min_elevation_gain: Option<f64>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub max_elevation_gain: Option<f64>,
255
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub page: Option<u32>,
259
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub page_size: Option<u32>,
263}
264
265impl RideWithGpsClient {
266 pub fn list_trips(&self, params: Option<&ListTripsParams>) -> Result<PaginatedResponse<Trip>> {
292 let mut url = "/api/v1/trips.json".to_string();
293
294 if let Some(params) = params {
295 let query = serde_json::to_value(params)?;
296 if let Some(obj) = query.as_object() {
297 if !obj.is_empty() {
298 let query_str = serde_urlencoded::to_string(obj).map_err(|e| {
299 crate::Error::ApiError(format!("Failed to encode query: {}", e))
300 })?;
301 url.push('?');
302 url.push_str(&query_str);
303 }
304 }
305 }
306
307 self.get(&url)
308 }
309
310 pub fn get_trip(&self, id: u64) -> Result<Trip> {
331 #[derive(Deserialize)]
332 struct TripWrapper {
333 trip: Trip,
334 }
335
336 let wrapper: TripWrapper = self.get(&format!("/api/v1/trips/{}.json", id))?;
337 Ok(wrapper.trip)
338 }
339
340 pub fn get_trip_polyline(&self, id: u64) -> Result<Polyline> {
361 self.get(&format!("/api/v1/trips/{}/polyline.json", id))
362 }
363
364 pub fn delete_trip(&self, id: u64) -> Result<()> {
384 self.delete(&format!("/api/v1/trips/{}.json", id))
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_trip_deserialization() {
394 let json = r#"{
395 "id": 456,
396 "name": "Morning Ride",
397 "distance": 25000.0,
398 "elevation_gain": 300.0,
399 "visibility": "private",
400 "duration": 3600.0,
401 "avg_speed": 6.94
402 }"#;
403
404 let trip: Trip = serde_json::from_str(json).unwrap();
405 assert_eq!(trip.id, 456);
406 assert_eq!(trip.name.as_deref(), Some("Morning Ride"));
407 assert_eq!(trip.distance, Some(25000.0));
408 assert_eq!(trip.visibility, Some(Visibility::Private));
409 assert_eq!(trip.duration, Some(3600.0));
410 }
411
412 #[test]
413 fn test_list_trips_params() {
414 let params = ListTripsParams {
415 name: Some("ride".to_string()),
416 visibility: Some(Visibility::Public),
417 min_distance: Some(10000.0),
418 page: Some(2),
419 ..Default::default()
420 };
421
422 let json = serde_json::to_value(¶ms).unwrap();
423 assert!(json.get("name").is_some());
424 assert!(json.get("visibility").is_some());
425 assert!(json.get("min_distance").is_some());
426 assert!(json.get("page").is_some());
427 }
428
429 #[test]
430 fn test_trip_wrapper_deserialization() {
431 let json = r#"{
432 "trip": {
433 "id": 789,
434 "name": "Wrapped Trip",
435 "distance": 30000.0,
436 "avg_hr": 145.5
437 }
438 }"#;
439
440 #[derive(Deserialize)]
441 struct TripWrapper {
442 trip: Trip,
443 }
444
445 let wrapper: TripWrapper = serde_json::from_str(json).unwrap();
446 assert_eq!(wrapper.trip.id, 789);
447 assert_eq!(wrapper.trip.name.as_deref(), Some("Wrapped Trip"));
448 assert_eq!(wrapper.trip.avg_hr, Some(145.5));
449 }
450
451 #[test]
452 fn test_trip_track_point_deserialization() {
453 let json = r#"{
454 "x": -122.4,
455 "y": 37.7,
456 "t": 1609459200,
457 "s": 5.5,
458 "h": 150.0,
459 "c": 90.0,
460 "p": 200.0,
461 "lap": true
462 }"#;
463
464 let track_point: TripTrackPoint = serde_json::from_str(json).unwrap();
465 assert_eq!(track_point.x, Some(-122.4));
466 assert_eq!(track_point.y, Some(37.7));
467 assert_eq!(track_point.t, Some(1609459200));
468 assert_eq!(track_point.s, Some(5.5));
469 assert_eq!(track_point.h, Some(150.0));
470 assert_eq!(track_point.c, Some(90.0));
471 assert_eq!(track_point.p, Some(200.0));
472 assert_eq!(track_point.lap, Some(true));
473 }
474
475 #[test]
476 fn test_gear_deserialization() {
477 let json = r#"{
478 "id": 42,
479 "make": "Trek",
480 "model": "Domane SL5",
481 "description": "Road Bike"
482 }"#;
483
484 let gear: Gear = serde_json::from_str(json).unwrap();
485 assert_eq!(gear.id, 42);
486 assert_eq!(gear.make.as_deref(), Some("Trek"));
487 assert_eq!(gear.model.as_deref(), Some("Domane SL5"));
488 assert_eq!(gear.description.as_deref(), Some("Road Bike"));
489 }
490
491 #[test]
492 fn test_trip_with_telemetry() {
493 let json = r#"{
494 "id": 555,
495 "name": "Power Training",
496 "avg_cad": 85.5,
497 "max_cad": 120.0,
498 "avg_hr": 155.0,
499 "max_hr": 180.0,
500 "avg_watts": 220.0,
501 "max_watts": 450.0,
502 "calories": 850.0,
503 "fit_sport": 2,
504 "fit_sub_sport": 10
505 }"#;
506
507 let trip: Trip = serde_json::from_str(json).unwrap();
508 assert_eq!(trip.id, 555);
509 assert_eq!(trip.avg_cad, Some(85.5));
510 assert_eq!(trip.max_cad, Some(120.0));
511 assert_eq!(trip.avg_hr, Some(155.0));
512 assert_eq!(trip.max_hr, Some(180.0));
513 assert_eq!(trip.avg_watts, Some(220.0));
514 assert_eq!(trip.calories, Some(850.0));
515 assert_eq!(trip.fit_sport, Some(2));
516 assert_eq!(trip.fit_sub_sport, Some(10));
517 }
518}