use crate::{PaginatedResponse, PointOfInterest, Result, RideWithGpsClient};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
Public,
Private,
Unlisted,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TrackPoint {
pub x: Option<f64>,
pub y: Option<f64>,
pub d: Option<f64>,
pub e: Option<f64>,
#[serde(rename = "S")]
pub surface: Option<i32>,
#[serde(rename = "R")]
pub highway: Option<i32>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CoursePoint {
pub x: Option<f64>,
pub y: Option<f64>,
pub d: Option<f64>,
pub t: Option<String>,
pub n: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Photo {
pub id: u64,
pub url: Option<String>,
pub highlighted: Option<bool>,
pub caption: Option<String>,
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Route {
pub id: u64,
pub name: Option<String>,
pub description: Option<String>,
pub distance: Option<f64>,
pub elevation_gain: Option<f64>,
pub elevation_loss: Option<f64>,
pub visibility: Option<Visibility>,
pub user_id: Option<u64>,
pub url: Option<String>,
pub html_url: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub locality: Option<String>,
pub administrative_area: Option<String>,
pub country_code: Option<String>,
pub track_type: Option<String>,
pub has_course_points: Option<bool>,
pub terrain: Option<String>,
pub difficulty: Option<String>,
pub first_lat: Option<f64>,
pub first_lng: Option<f64>,
pub last_lat: Option<f64>,
pub last_lng: Option<f64>,
pub sw_lat: Option<f64>,
pub sw_lng: Option<f64>,
pub ne_lat: Option<f64>,
pub ne_lng: Option<f64>,
pub unpaved_pct: Option<f64>,
pub surface: Option<String>,
pub archived: Option<bool>,
pub activity_types: Option<Vec<String>>,
pub track_points: Option<Vec<TrackPoint>>,
pub course_points: Option<Vec<CoursePoint>>,
pub points_of_interest: Option<Vec<PointOfInterest>>,
pub photos: Option<Vec<Photo>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Polyline {
pub polyline: String,
pub parent_type: Option<String>,
pub parent_id: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ListRoutesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<Visibility>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_distance: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_distance: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_elevation_gain: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_elevation_gain: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_size: Option<u32>,
}
impl RideWithGpsClient {
pub fn list_routes(
&self,
params: Option<&ListRoutesParams>,
) -> Result<PaginatedResponse<Route>> {
let mut url = "/api/v1/routes.json".to_string();
if let Some(params) = params {
let query = serde_json::to_value(params)?;
if let Some(obj) = query.as_object() {
if !obj.is_empty() {
let query_str = serde_urlencoded::to_string(obj).map_err(|e| {
crate::Error::ApiError(format!("Failed to encode query: {}", e))
})?;
url.push('?');
url.push_str(&query_str);
}
}
}
self.get(&url)
}
pub fn get_route(&self, id: u64) -> Result<Route> {
#[derive(Deserialize)]
struct RouteWrapper {
route: Route,
}
let wrapper: RouteWrapper = self.get(&format!("/api/v1/routes/{}.json", id))?;
Ok(wrapper.route)
}
pub fn get_route_polyline(&self, id: u64) -> Result<Polyline> {
self.get(&format!("/api/v1/routes/{}/polyline.json", id))
}
pub fn delete_route(&self, id: u64) -> Result<()> {
self.delete(&format!("/api/v1/routes/{}.json", id))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_route_deserialization() {
let json = r#"{
"id": 123,
"name": "Test Route",
"distance": 10000.0,
"elevation_gain": 500.0,
"visibility": "public"
}"#;
let route: Route = serde_json::from_str(json).unwrap();
assert_eq!(route.id, 123);
assert_eq!(route.name.as_deref(), Some("Test Route"));
assert_eq!(route.distance, Some(10000.0));
assert_eq!(route.visibility, Some(Visibility::Public));
}
#[test]
fn test_polyline_deserialization() {
let json = r#"{
"polyline": "encoded_string_here",
"parent_type": "route",
"parent_id": 123
}"#;
let polyline: Polyline = serde_json::from_str(json).unwrap();
assert_eq!(polyline.polyline, "encoded_string_here");
assert_eq!(polyline.parent_type.as_deref(), Some("route"));
assert_eq!(polyline.parent_id, Some(123));
}
#[test]
fn test_list_routes_params() {
let params = ListRoutesParams {
name: Some("test".to_string()),
visibility: Some(Visibility::Public),
min_distance: Some(5000.0),
..Default::default()
};
let json = serde_json::to_value(¶ms).unwrap();
assert!(json.get("name").is_some());
assert!(json.get("visibility").is_some());
assert!(json.get("min_distance").is_some());
}
#[test]
fn test_route_wrapper_deserialization() {
let json = r#"{
"route": {
"id": 456,
"name": "Wrapped Route",
"distance": 15000.0
}
}"#;
#[derive(Deserialize)]
struct RouteWrapper {
route: Route,
}
let wrapper: RouteWrapper = serde_json::from_str(json).unwrap();
assert_eq!(wrapper.route.id, 456);
assert_eq!(wrapper.route.name.as_deref(), Some("Wrapped Route"));
assert_eq!(wrapper.route.distance, Some(15000.0));
}
#[test]
fn test_track_point_deserialization() {
let json = r#"{
"x": -122.4194,
"y": 37.7749,
"d": 1234.5,
"e": 100.0,
"S": 2,
"R": 3
}"#;
let track_point: TrackPoint = serde_json::from_str(json).unwrap();
assert_eq!(track_point.x, Some(-122.4194));
assert_eq!(track_point.y, Some(37.7749));
assert_eq!(track_point.d, Some(1234.5));
assert_eq!(track_point.e, Some(100.0));
assert_eq!(track_point.surface, Some(2));
assert_eq!(track_point.highway, Some(3));
}
#[test]
fn test_course_point_deserialization() {
let json = r#"{
"x": -122.5,
"y": 37.8,
"d": 5000.0,
"n": "Water Stop",
"t": "water"
}"#;
let course_point: CoursePoint = serde_json::from_str(json).unwrap();
assert_eq!(course_point.x, Some(-122.5));
assert_eq!(course_point.y, Some(37.8));
assert_eq!(course_point.d, Some(5000.0));
assert_eq!(course_point.n.as_deref(), Some("Water Stop"));
assert_eq!(course_point.t.as_deref(), Some("water"));
}
#[test]
fn test_route_with_nested_structures() {
let json = r#"{
"id": 999,
"name": "Complex Route",
"track_points": [
{"x": -122.0, "y": 37.0, "d": 0.0},
{"x": -122.1, "y": 37.1, "d": 100.0}
],
"course_points": [
{"id": 1, "n": "Start", "t": "generic"}
]
}"#;
let route: Route = serde_json::from_str(json).unwrap();
assert_eq!(route.id, 999);
assert!(route.track_points.is_some());
assert_eq!(route.track_points.as_ref().unwrap().len(), 2);
assert!(route.course_points.is_some());
assert_eq!(route.course_points.as_ref().unwrap().len(), 1);
}
#[test]
fn test_photo_deserialization() {
let json = r#"{
"id": 111,
"url": "https://example.com/photo.jpg",
"thumbnail_url": "https://example.com/thumb.jpg",
"caption": "Great view"
}"#;
let photo: Photo = serde_json::from_str(json).unwrap();
assert_eq!(photo.id, 111);
assert_eq!(photo.url.as_deref(), Some("https://example.com/photo.jpg"));
assert_eq!(photo.caption.as_deref(), Some("Great view"));
}
}