ridewithgps_client/
routes.rs

1//! Route-related types and methods
2
3use crate::{PaginatedResponse, PointOfInterest, Result, RideWithGpsClient};
4use serde::{Deserialize, Serialize};
5
6/// Visibility setting for a route
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum Visibility {
10    /// Public route
11    Public,
12
13    /// Private route
14    Private,
15
16    /// Unlisted route
17    Unlisted,
18}
19
20/// Track point on a route
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct TrackPoint {
23    /// Longitude
24    pub x: Option<f64>,
25
26    /// Latitude
27    pub y: Option<f64>,
28
29    /// Distance in meters
30    pub d: Option<f64>,
31
32    /// Elevation in meters
33    pub e: Option<f64>,
34
35    /// Surface type
36    #[serde(rename = "S")]
37    pub surface: Option<i32>,
38
39    /// Highway tag
40    #[serde(rename = "R")]
41    pub highway: Option<i32>,
42}
43
44/// Course point (turn-by-turn cue) on a route
45#[derive(Debug, Clone, Deserialize, Serialize)]
46pub struct CoursePoint {
47    /// Longitude
48    pub x: Option<f64>,
49
50    /// Latitude
51    pub y: Option<f64>,
52
53    /// Distance in meters
54    pub d: Option<f64>,
55
56    /// Cue type
57    pub t: Option<String>,
58
59    /// Cue text/description
60    pub n: Option<String>,
61}
62
63/// Photo attached to a route or trip
64#[derive(Debug, Clone, Deserialize, Serialize)]
65pub struct Photo {
66    /// Photo ID
67    pub id: u64,
68
69    /// Photo URL
70    pub url: Option<String>,
71
72    /// Whether the photo is highlighted
73    pub highlighted: Option<bool>,
74
75    /// Photo caption
76    pub caption: Option<String>,
77
78    /// Created timestamp
79    pub created_at: Option<String>,
80}
81
82/// A route
83#[derive(Debug, Clone, Deserialize, Serialize)]
84pub struct Route {
85    /// Route ID
86    pub id: u64,
87
88    /// Route name
89    pub name: Option<String>,
90
91    /// Route description
92    pub description: Option<String>,
93
94    /// Distance in meters
95    pub distance: Option<f64>,
96
97    /// Elevation gain in meters
98    pub elevation_gain: Option<f64>,
99
100    /// Elevation loss in meters
101    pub elevation_loss: Option<f64>,
102
103    /// Route visibility
104    pub visibility: Option<Visibility>,
105
106    /// User ID of the route owner
107    pub user_id: Option<u64>,
108
109    /// API URL
110    pub url: Option<String>,
111
112    /// HTML/web URL
113    pub html_url: Option<String>,
114
115    /// Created timestamp
116    pub created_at: Option<String>,
117
118    /// Updated timestamp
119    pub updated_at: Option<String>,
120
121    /// Locality/location
122    pub locality: Option<String>,
123
124    /// Administrative area
125    pub administrative_area: Option<String>,
126
127    /// Country code
128    pub country_code: Option<String>,
129
130    /// Track type
131    pub track_type: Option<String>,
132
133    /// Whether the route has course points
134    pub has_course_points: Option<bool>,
135
136    /// Terrain rating
137    pub terrain: Option<String>,
138
139    /// Difficulty rating
140    pub difficulty: Option<String>,
141
142    /// First point latitude
143    pub first_lat: Option<f64>,
144
145    /// First point longitude
146    pub first_lng: Option<f64>,
147
148    /// Last point latitude
149    pub last_lat: Option<f64>,
150
151    /// Last point longitude
152    pub last_lng: Option<f64>,
153
154    /// Southwest corner latitude (bounding box)
155    pub sw_lat: Option<f64>,
156
157    /// Southwest corner longitude (bounding box)
158    pub sw_lng: Option<f64>,
159
160    /// Northeast corner latitude (bounding box)
161    pub ne_lat: Option<f64>,
162
163    /// Northeast corner longitude (bounding box)
164    pub ne_lng: Option<f64>,
165
166    /// Percentage of unpaved surface
167    pub unpaved_pct: Option<f64>,
168
169    /// Surface type
170    pub surface: Option<String>,
171
172    /// Whether the route is archived
173    pub archived: Option<bool>,
174
175    /// Activity types
176    pub activity_types: Option<Vec<String>>,
177
178    /// Track points (included when fetching a specific route)
179    pub track_points: Option<Vec<TrackPoint>>,
180
181    /// Course points/cues (included when fetching a specific route)
182    pub course_points: Option<Vec<CoursePoint>>,
183
184    /// Points of interest along the route (included when fetching a specific route)
185    pub points_of_interest: Option<Vec<PointOfInterest>>,
186
187    /// Photos (included when fetching a specific route)
188    pub photos: Option<Vec<Photo>>,
189}
190
191/// Polyline data for a route
192#[derive(Debug, Clone, Deserialize, Serialize)]
193pub struct Polyline {
194    /// Encoded polyline string
195    pub polyline: String,
196
197    /// Parent type (e.g., "route")
198    pub parent_type: Option<String>,
199
200    /// Parent ID
201    pub parent_id: Option<u64>,
202}
203
204/// Parameters for listing routes
205#[derive(Debug, Clone, Default, Serialize)]
206pub struct ListRoutesParams {
207    /// Filter by route name
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub name: Option<String>,
210
211    /// Filter by visibility
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub visibility: Option<Visibility>,
214
215    /// Filter by minimum distance (meters)
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub min_distance: Option<f64>,
218
219    /// Filter by maximum distance (meters)
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub max_distance: Option<f64>,
222
223    /// Filter by minimum elevation gain (meters)
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub min_elevation_gain: Option<f64>,
226
227    /// Filter by maximum elevation gain (meters)
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub max_elevation_gain: Option<f64>,
230
231    /// Page number
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub page: Option<u32>,
234
235    /// Page size
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub page_size: Option<u32>,
238}
239
240impl RideWithGpsClient {
241    /// List routes for the authenticated user
242    ///
243    /// # Arguments
244    ///
245    /// * `params` - Optional parameters for filtering and pagination
246    ///
247    /// # Example
248    ///
249    /// ```rust,no_run
250    /// use ridewithgps_client::{RideWithGpsClient, ListRoutesParams};
251    ///
252    /// let client = RideWithGpsClient::new(
253    ///     "https://ridewithgps.com",
254    ///     "your-api-key",
255    ///     Some("your-auth-token")
256    /// );
257    ///
258    /// let params = ListRoutesParams {
259    ///     min_distance: Some(10000.0), // 10km
260    ///     ..Default::default()
261    /// };
262    ///
263    /// let routes = client.list_routes(Some(&params)).unwrap();
264    /// println!("Found {} routes", routes.results.len());
265    /// ```
266    pub fn list_routes(
267        &self,
268        params: Option<&ListRoutesParams>,
269    ) -> Result<PaginatedResponse<Route>> {
270        let mut url = "/api/v1/routes.json".to_string();
271
272        if let Some(params) = params {
273            let query = serde_json::to_value(params)?;
274            if let Some(obj) = query.as_object() {
275                if !obj.is_empty() {
276                    let query_str = serde_urlencoded::to_string(obj).map_err(|e| {
277                        crate::Error::ApiError(format!("Failed to encode query: {}", e))
278                    })?;
279                    url.push('?');
280                    url.push_str(&query_str);
281                }
282            }
283        }
284
285        self.get(&url)
286    }
287
288    /// Get a specific route by ID
289    ///
290    /// # Arguments
291    ///
292    /// * `id` - The route ID
293    ///
294    /// # Example
295    ///
296    /// ```rust,no_run
297    /// use ridewithgps_client::RideWithGpsClient;
298    ///
299    /// let client = RideWithGpsClient::new(
300    ///     "https://ridewithgps.com",
301    ///     "your-api-key",
302    ///     Some("your-auth-token")
303    /// );
304    ///
305    /// let route = client.get_route(12345).unwrap();
306    /// println!("Route: {:?}", route);
307    /// ```
308    pub fn get_route(&self, id: u64) -> Result<Route> {
309        #[derive(Deserialize)]
310        struct RouteWrapper {
311            route: Route,
312        }
313
314        let wrapper: RouteWrapper = self.get(&format!("/api/v1/routes/{}.json", id))?;
315        Ok(wrapper.route)
316    }
317
318    /// Get the polyline for a specific route
319    ///
320    /// # Arguments
321    ///
322    /// * `id` - The route ID
323    ///
324    /// # Example
325    ///
326    /// ```rust,no_run
327    /// use ridewithgps_client::RideWithGpsClient;
328    ///
329    /// let client = RideWithGpsClient::new(
330    ///     "https://ridewithgps.com",
331    ///     "your-api-key",
332    ///     None
333    /// );
334    ///
335    /// let polyline = client.get_route_polyline(12345).unwrap();
336    /// println!("Polyline: {}", polyline.polyline);
337    /// ```
338    pub fn get_route_polyline(&self, id: u64) -> Result<Polyline> {
339        self.get(&format!("/api/v1/routes/{}/polyline.json", id))
340    }
341
342    /// Delete a route
343    ///
344    /// # Arguments
345    ///
346    /// * `id` - The route ID
347    ///
348    /// # Example
349    ///
350    /// ```rust,no_run
351    /// use ridewithgps_client::RideWithGpsClient;
352    ///
353    /// let client = RideWithGpsClient::new(
354    ///     "https://ridewithgps.com",
355    ///     "your-api-key",
356    ///     Some("your-auth-token")
357    /// );
358    ///
359    /// client.delete_route(12345).unwrap();
360    /// ```
361    pub fn delete_route(&self, id: u64) -> Result<()> {
362        self.delete(&format!("/api/v1/routes/{}.json", id))
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_route_deserialization() {
372        let json = r#"{
373            "id": 123,
374            "name": "Test Route",
375            "distance": 10000.0,
376            "elevation_gain": 500.0,
377            "visibility": "public"
378        }"#;
379
380        let route: Route = serde_json::from_str(json).unwrap();
381        assert_eq!(route.id, 123);
382        assert_eq!(route.name.as_deref(), Some("Test Route"));
383        assert_eq!(route.distance, Some(10000.0));
384        assert_eq!(route.visibility, Some(Visibility::Public));
385    }
386
387    #[test]
388    fn test_polyline_deserialization() {
389        let json = r#"{
390            "polyline": "encoded_string_here",
391            "parent_type": "route",
392            "parent_id": 123
393        }"#;
394
395        let polyline: Polyline = serde_json::from_str(json).unwrap();
396        assert_eq!(polyline.polyline, "encoded_string_here");
397        assert_eq!(polyline.parent_type.as_deref(), Some("route"));
398        assert_eq!(polyline.parent_id, Some(123));
399    }
400
401    #[test]
402    fn test_list_routes_params() {
403        let params = ListRoutesParams {
404            name: Some("test".to_string()),
405            visibility: Some(Visibility::Public),
406            min_distance: Some(5000.0),
407            ..Default::default()
408        };
409
410        let json = serde_json::to_value(&params).unwrap();
411        assert!(json.get("name").is_some());
412        assert!(json.get("visibility").is_some());
413        assert!(json.get("min_distance").is_some());
414    }
415
416    #[test]
417    fn test_route_wrapper_deserialization() {
418        let json = r#"{
419            "route": {
420                "id": 456,
421                "name": "Wrapped Route",
422                "distance": 15000.0
423            }
424        }"#;
425
426        #[derive(Deserialize)]
427        struct RouteWrapper {
428            route: Route,
429        }
430
431        let wrapper: RouteWrapper = serde_json::from_str(json).unwrap();
432        assert_eq!(wrapper.route.id, 456);
433        assert_eq!(wrapper.route.name.as_deref(), Some("Wrapped Route"));
434        assert_eq!(wrapper.route.distance, Some(15000.0));
435    }
436
437    #[test]
438    fn test_track_point_deserialization() {
439        let json = r#"{
440            "x": -122.4194,
441            "y": 37.7749,
442            "d": 1234.5,
443            "e": 100.0,
444            "S": 2,
445            "R": 3
446        }"#;
447
448        let track_point: TrackPoint = serde_json::from_str(json).unwrap();
449        assert_eq!(track_point.x, Some(-122.4194));
450        assert_eq!(track_point.y, Some(37.7749));
451        assert_eq!(track_point.d, Some(1234.5));
452        assert_eq!(track_point.e, Some(100.0));
453        assert_eq!(track_point.surface, Some(2));
454        assert_eq!(track_point.highway, Some(3));
455    }
456
457    #[test]
458    fn test_course_point_deserialization() {
459        let json = r#"{
460            "x": -122.5,
461            "y": 37.8,
462            "d": 5000.0,
463            "n": "Water Stop",
464            "t": "water"
465        }"#;
466
467        let course_point: CoursePoint = serde_json::from_str(json).unwrap();
468        assert_eq!(course_point.x, Some(-122.5));
469        assert_eq!(course_point.y, Some(37.8));
470        assert_eq!(course_point.d, Some(5000.0));
471        assert_eq!(course_point.n.as_deref(), Some("Water Stop"));
472        assert_eq!(course_point.t.as_deref(), Some("water"));
473    }
474
475    #[test]
476    fn test_route_with_nested_structures() {
477        let json = r#"{
478            "id": 999,
479            "name": "Complex Route",
480            "track_points": [
481                {"x": -122.0, "y": 37.0, "d": 0.0},
482                {"x": -122.1, "y": 37.1, "d": 100.0}
483            ],
484            "course_points": [
485                {"id": 1, "n": "Start", "t": "generic"}
486            ]
487        }"#;
488
489        let route: Route = serde_json::from_str(json).unwrap();
490        assert_eq!(route.id, 999);
491        assert!(route.track_points.is_some());
492        assert_eq!(route.track_points.as_ref().unwrap().len(), 2);
493        assert!(route.course_points.is_some());
494        assert_eq!(route.course_points.as_ref().unwrap().len(), 1);
495    }
496
497    #[test]
498    fn test_photo_deserialization() {
499        let json = r#"{
500            "id": 111,
501            "url": "https://example.com/photo.jpg",
502            "thumbnail_url": "https://example.com/thumb.jpg",
503            "caption": "Great view"
504        }"#;
505
506        let photo: Photo = serde_json::from_str(json).unwrap();
507        assert_eq!(photo.id, 111);
508        assert_eq!(photo.url.as_deref(), Some("https://example.com/photo.jpg"));
509        assert_eq!(photo.caption.as_deref(), Some("Great view"));
510    }
511}