ridewithgps_client/
collections.rs

1//! Collection-related types and methods
2
3use crate::{PaginatedResponse, Result, RideWithGpsClient, Route, Trip};
4use serde::{Deserialize, Serialize};
5
6/// A collection of routes and trips
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct Collection {
9    /// Collection ID
10    pub id: u64,
11
12    /// Collection name
13    pub name: Option<String>,
14
15    /// Collection description
16    pub description: Option<String>,
17
18    /// User ID of the collection owner
19    pub user_id: Option<u64>,
20
21    /// Visibility
22    pub visibility: Option<crate::Visibility>,
23
24    /// API URL
25    pub url: Option<String>,
26
27    /// HTML/web URL
28    pub html_url: Option<String>,
29
30    /// Cover photo/image
31    pub cover: Option<String>,
32
33    /// Created timestamp
34    pub created_at: Option<String>,
35
36    /// Updated timestamp
37    pub updated_at: Option<String>,
38
39    /// Number of routes in the collection
40    pub route_count: Option<u32>,
41
42    /// Collection cover photo URL
43    pub cover_photo_url: Option<String>,
44
45    /// Routes in the collection (included when fetching a specific collection)
46    pub routes: Option<Vec<Route>>,
47
48    /// Trips in the collection (included when fetching a specific collection)
49    pub trips: Option<Vec<Trip>>,
50}
51
52/// Parameters for listing collections
53#[derive(Debug, Clone, Default, Serialize)]
54pub struct ListCollectionsParams {
55    /// Filter by collection name
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub name: Option<String>,
58
59    /// Page number
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub page: Option<u32>,
62
63    /// Page size
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub page_size: Option<u32>,
66}
67
68impl RideWithGpsClient {
69    /// List collections
70    ///
71    /// # Arguments
72    ///
73    /// * `params` - Optional parameters for filtering and pagination
74    ///
75    /// # Example
76    ///
77    /// ```rust,no_run
78    /// use ridewithgps_client::RideWithGpsClient;
79    ///
80    /// let client = RideWithGpsClient::new(
81    ///     "https://ridewithgps.com",
82    ///     "your-api-key",
83    ///     Some("your-auth-token")
84    /// );
85    ///
86    /// let collections = client.list_collections(None).unwrap();
87    /// println!("Found {} collections", collections.results.len());
88    /// ```
89    pub fn list_collections(
90        &self,
91        params: Option<&ListCollectionsParams>,
92    ) -> Result<PaginatedResponse<Collection>> {
93        let mut url = "/api/v1/collections.json".to_string();
94
95        if let Some(params) = params {
96            let query = serde_json::to_value(params)?;
97            if let Some(obj) = query.as_object() {
98                if !obj.is_empty() {
99                    let query_str = serde_urlencoded::to_string(obj).map_err(|e| {
100                        crate::Error::ApiError(format!("Failed to encode query: {}", e))
101                    })?;
102                    url.push('?');
103                    url.push_str(&query_str);
104                }
105            }
106        }
107
108        self.get(&url)
109    }
110
111    /// Get a specific collection by ID
112    ///
113    /// This returns the full collection including its routes and trips.
114    ///
115    /// # Arguments
116    ///
117    /// * `id` - The collection ID
118    ///
119    /// # Example
120    ///
121    /// ```rust,no_run
122    /// use ridewithgps_client::RideWithGpsClient;
123    ///
124    /// let client = RideWithGpsClient::new(
125    ///     "https://ridewithgps.com",
126    ///     "your-api-key",
127    ///     None
128    /// );
129    ///
130    /// let collection = client.get_collection(12345).unwrap();
131    /// println!("Collection: {:?}", collection);
132    ///
133    /// // Access routes within the collection
134    /// if let Some(routes) = &collection.routes {
135    ///     for route in routes {
136    ///         println!("Route: {} - {:?}", route.id, route.name);
137    ///     }
138    /// }
139    ///
140    /// // Access trips within the collection
141    /// if let Some(trips) = &collection.trips {
142    ///     for trip in trips {
143    ///         println!("Trip: {} - {:?}", trip.id, trip.name);
144    ///     }
145    /// }
146    /// ```
147    pub fn get_collection(&self, id: u64) -> Result<Collection> {
148        #[derive(Deserialize)]
149        struct CollectionWrapper {
150            collection: Collection,
151        }
152
153        let wrapper: CollectionWrapper = self.get(&format!("/api/v1/collections/{}.json", id))?;
154        Ok(wrapper.collection)
155    }
156
157    /// Get the pinned collection
158    ///
159    /// # Example
160    ///
161    /// ```rust,no_run
162    /// use ridewithgps_client::RideWithGpsClient;
163    ///
164    /// let client = RideWithGpsClient::new(
165    ///     "https://ridewithgps.com",
166    ///     "your-api-key",
167    ///     Some("your-auth-token")
168    /// );
169    ///
170    /// let collection = client.get_pinned_collection().unwrap();
171    /// println!("Pinned collection: {:?}", collection);
172    /// ```
173    pub fn get_pinned_collection(&self) -> Result<Collection> {
174        #[derive(Deserialize)]
175        struct CollectionWrapper {
176            collection: Collection,
177        }
178
179        let wrapper: CollectionWrapper = self.get("/api/v1/collections/pinned.json")?;
180        Ok(wrapper.collection)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_collection_deserialization() {
190        let json = r#"{
191            "id": 999,
192            "name": "My Favorite Routes",
193            "description": "Best rides in the area",
194            "user_id": 123,
195            "route_count": 15
196        }"#;
197
198        let collection: Collection = serde_json::from_str(json).unwrap();
199        assert_eq!(collection.id, 999);
200        assert_eq!(collection.name.as_deref(), Some("My Favorite Routes"));
201        assert_eq!(
202            collection.description.as_deref(),
203            Some("Best rides in the area")
204        );
205        assert_eq!(collection.route_count, Some(15));
206    }
207
208    #[test]
209    fn test_list_collections_params() {
210        let params = ListCollectionsParams {
211            name: Some("favorites".to_string()),
212            page: Some(1),
213            ..Default::default()
214        };
215
216        let json = serde_json::to_value(&params).unwrap();
217        assert!(json.get("name").is_some());
218        assert!(json.get("page").is_some());
219    }
220
221    #[test]
222    fn test_collection_wrapper_deserialization() {
223        let json = r#"{
224            "collection": {
225                "id": 888,
226                "name": "Wrapped Collection",
227                "visibility": "public"
228            }
229        }"#;
230
231        #[derive(Deserialize)]
232        struct CollectionWrapper {
233            collection: Collection,
234        }
235
236        let wrapper: CollectionWrapper = serde_json::from_str(json).unwrap();
237        assert_eq!(wrapper.collection.id, 888);
238        assert_eq!(
239            wrapper.collection.name.as_deref(),
240            Some("Wrapped Collection")
241        );
242        assert_eq!(
243            wrapper.collection.visibility,
244            Some(crate::Visibility::Public)
245        );
246    }
247
248    #[test]
249    fn test_collection_with_nested_routes() {
250        let json = r#"{
251            "id": 777,
252            "name": "Collection with Routes",
253            "routes": [
254                {
255                    "id": 1,
256                    "name": "Route 1",
257                    "distance": 10000.0
258                },
259                {
260                    "id": 2,
261                    "name": "Route 2",
262                    "distance": 20000.0
263                }
264            ]
265        }"#;
266
267        let collection: Collection = serde_json::from_str(json).unwrap();
268        assert_eq!(collection.id, 777);
269        assert!(collection.routes.is_some());
270        let routes = collection.routes.unwrap();
271        assert_eq!(routes.len(), 2);
272        assert_eq!(routes[0].id, 1);
273        assert_eq!(routes[0].name.as_deref(), Some("Route 1"));
274        assert_eq!(routes[1].id, 2);
275    }
276
277    #[test]
278    fn test_collection_with_nested_trips() {
279        let json = r#"{
280            "id": 666,
281            "name": "Collection with Trips",
282            "trips": [
283                {
284                    "id": 10,
285                    "name": "Trip 1",
286                    "distance": 15000.0
287                }
288            ]
289        }"#;
290
291        let collection: Collection = serde_json::from_str(json).unwrap();
292        assert_eq!(collection.id, 666);
293        assert!(collection.trips.is_some());
294        let trips = collection.trips.unwrap();
295        assert_eq!(trips.len(), 1);
296        assert_eq!(trips[0].id, 10);
297        assert_eq!(trips[0].name.as_deref(), Some("Trip 1"));
298    }
299}