ridewithgps_client/poi.rs
1//! Points of Interest related types and methods
2//!
3//! Note: These endpoints are only available to organization accounts.
4
5use crate::{PaginatedResponse, Result, RideWithGpsClient};
6use serde::{Deserialize, Serialize};
7
8/// A point of interest
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct PointOfInterest {
11 /// POI ID
12 pub id: u64,
13
14 /// POI name
15 pub name: Option<String>,
16
17 /// POI description
18 pub description: Option<String>,
19
20 /// Latitude
21 #[serde(alias = "latitude")]
22 pub lat: Option<f64>,
23
24 /// Longitude
25 #[serde(alias = "longitude")]
26 pub lng: Option<f64>,
27
28 /// POI type/category
29 #[serde(alias = "poi_type")]
30 pub r#type: Option<String>,
31
32 /// Type ID
33 pub type_id: Option<u64>,
34
35 /// Type name
36 pub type_name: Option<String>,
37
38 /// Icon identifier
39 pub icon: Option<String>,
40
41 /// User ID of the POI owner
42 pub user_id: Option<u64>,
43
44 /// Organization ID
45 pub organization_id: Option<u64>,
46
47 /// API URL
48 pub url: Option<String>,
49
50 /// Created timestamp
51 pub created_at: Option<String>,
52
53 /// Updated timestamp
54 pub updated_at: Option<String>,
55
56 /// Address
57 pub address: Option<String>,
58
59 /// Phone number
60 pub phone: Option<String>,
61
62 /// Website URL
63 pub website: Option<String>,
64
65 /// Tag names
66 pub tag_names: Option<Vec<String>>,
67}
68
69/// Parameters for listing POIs
70#[derive(Debug, Clone, Default, Serialize)]
71pub struct ListPointsOfInterestParams {
72 /// Filter by POI name
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub name: Option<String>,
75
76 /// Filter by POI type
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub poi_type: Option<String>,
79
80 /// Page number
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub page: Option<u32>,
83
84 /// Page size
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub page_size: Option<u32>,
87}
88
89/// Request to create or update a POI
90#[derive(Debug, Clone, Serialize)]
91pub struct PointOfInterestRequest {
92 /// POI name
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub name: Option<String>,
95
96 /// POI description
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub description: Option<String>,
99
100 /// Latitude
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub latitude: Option<f64>,
103
104 /// Longitude
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub longitude: Option<f64>,
107
108 /// POI type/category
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub poi_type: Option<String>,
111
112 /// Icon identifier
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub icon: Option<String>,
115
116 /// Address
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub address: Option<String>,
119
120 /// Phone number
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub phone: Option<String>,
123
124 /// Website URL
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub website: Option<String>,
127}
128
129impl RideWithGpsClient {
130 /// List points of interest
131 ///
132 /// Note: This endpoint is only available to organization accounts.
133 ///
134 /// # Arguments
135 ///
136 /// * `params` - Optional parameters for filtering and pagination
137 ///
138 /// # Example
139 ///
140 /// ```rust,no_run
141 /// use ridewithgps_client::RideWithGpsClient;
142 ///
143 /// let client = RideWithGpsClient::new(
144 /// "https://ridewithgps.com",
145 /// "your-api-key",
146 /// Some("your-auth-token")
147 /// );
148 ///
149 /// let pois = client.list_points_of_interest(None).unwrap();
150 /// println!("Found {} POIs", pois.results.len());
151 /// ```
152 pub fn list_points_of_interest(
153 &self,
154 params: Option<&ListPointsOfInterestParams>,
155 ) -> Result<PaginatedResponse<PointOfInterest>> {
156 let mut url = "/api/v1/points_of_interest.json".to_string();
157
158 if let Some(params) = params {
159 let query = serde_json::to_value(params)?;
160 if let Some(obj) = query.as_object() {
161 if !obj.is_empty() {
162 let query_str = serde_urlencoded::to_string(obj).map_err(|e| {
163 crate::Error::ApiError(format!("Failed to encode query: {}", e))
164 })?;
165 url.push('?');
166 url.push_str(&query_str);
167 }
168 }
169 }
170
171 self.get(&url)
172 }
173
174 /// Create a new point of interest
175 ///
176 /// Note: This endpoint is only available to organization accounts.
177 ///
178 /// # Arguments
179 ///
180 /// * `poi` - The POI data
181 ///
182 /// # Example
183 ///
184 /// ```rust,no_run
185 /// use ridewithgps_client::{RideWithGpsClient, PointOfInterestRequest};
186 ///
187 /// let client = RideWithGpsClient::new(
188 /// "https://ridewithgps.com",
189 /// "your-api-key",
190 /// Some("your-auth-token")
191 /// );
192 ///
193 /// let poi_req = PointOfInterestRequest {
194 /// name: Some("Coffee Shop".to_string()),
195 /// description: Some("Great coffee stop".to_string()),
196 /// latitude: Some(37.7749),
197 /// longitude: Some(-122.4194),
198 /// poi_type: Some("cafe".to_string()),
199 /// icon: Some("coffee".to_string()),
200 /// address: None,
201 /// phone: None,
202 /// website: None,
203 /// };
204 ///
205 /// let poi = client.create_point_of_interest(&poi_req).unwrap();
206 /// println!("Created POI: {}", poi.id);
207 /// ```
208 pub fn create_point_of_interest(
209 &self,
210 poi: &PointOfInterestRequest,
211 ) -> Result<PointOfInterest> {
212 #[derive(Deserialize)]
213 struct PoiWrapper {
214 point_of_interest: PointOfInterest,
215 }
216
217 let wrapper: PoiWrapper = self.post("/api/v1/points_of_interest.json", poi)?;
218 Ok(wrapper.point_of_interest)
219 }
220
221 /// Get a specific point of interest by ID
222 ///
223 /// Note: This endpoint is only available to organization accounts.
224 ///
225 /// # Arguments
226 ///
227 /// * `id` - The POI ID
228 ///
229 /// # Example
230 ///
231 /// ```rust,no_run
232 /// use ridewithgps_client::RideWithGpsClient;
233 ///
234 /// let client = RideWithGpsClient::new(
235 /// "https://ridewithgps.com",
236 /// "your-api-key",
237 /// Some("your-auth-token")
238 /// );
239 ///
240 /// let poi = client.get_point_of_interest(12345).unwrap();
241 /// println!("POI: {:?}", poi);
242 /// ```
243 pub fn get_point_of_interest(&self, id: u64) -> Result<PointOfInterest> {
244 #[derive(Deserialize)]
245 struct PoiWrapper {
246 point_of_interest: PointOfInterest,
247 }
248
249 let wrapper: PoiWrapper = self.get(&format!("/api/v1/points_of_interest/{}.json", id))?;
250 Ok(wrapper.point_of_interest)
251 }
252
253 /// Update a point of interest
254 ///
255 /// Note: This endpoint is only available to organization accounts.
256 ///
257 /// # Arguments
258 ///
259 /// * `id` - The POI ID
260 /// * `poi` - The updated POI data
261 ///
262 /// # Example
263 ///
264 /// ```rust,no_run
265 /// use ridewithgps_client::{RideWithGpsClient, PointOfInterestRequest};
266 ///
267 /// let client = RideWithGpsClient::new(
268 /// "https://ridewithgps.com",
269 /// "your-api-key",
270 /// Some("your-auth-token")
271 /// );
272 ///
273 /// let poi_req = PointOfInterestRequest {
274 /// name: Some("Updated Coffee Shop".to_string()),
275 /// description: None,
276 /// latitude: None,
277 /// longitude: None,
278 /// poi_type: None,
279 /// icon: None,
280 /// address: None,
281 /// phone: None,
282 /// website: None,
283 /// };
284 ///
285 /// let poi = client.update_point_of_interest(12345, &poi_req).unwrap();
286 /// println!("Updated POI: {:?}", poi);
287 /// ```
288 pub fn update_point_of_interest(
289 &self,
290 id: u64,
291 poi: &PointOfInterestRequest,
292 ) -> Result<PointOfInterest> {
293 #[derive(Deserialize)]
294 struct PoiWrapper {
295 point_of_interest: PointOfInterest,
296 }
297
298 let wrapper: PoiWrapper =
299 self.put(&format!("/api/v1/points_of_interest/{}.json", id), poi)?;
300 Ok(wrapper.point_of_interest)
301 }
302
303 /// Delete a point of interest
304 ///
305 /// Note: This endpoint is only available to organization accounts.
306 ///
307 /// # Arguments
308 ///
309 /// * `id` - The POI ID
310 ///
311 /// # Example
312 ///
313 /// ```rust,no_run
314 /// use ridewithgps_client::RideWithGpsClient;
315 ///
316 /// let client = RideWithGpsClient::new(
317 /// "https://ridewithgps.com",
318 /// "your-api-key",
319 /// Some("your-auth-token")
320 /// );
321 ///
322 /// client.delete_point_of_interest(12345).unwrap();
323 /// ```
324 pub fn delete_point_of_interest(&self, id: u64) -> Result<()> {
325 self.delete(&format!("/api/v1/points_of_interest/{}.json", id))
326 }
327
328 /// Associate a point of interest with a route
329 ///
330 /// Note: This endpoint is only available to organization accounts.
331 ///
332 /// # Arguments
333 ///
334 /// * `poi_id` - The POI ID
335 /// * `route_id` - The route ID
336 ///
337 /// # Example
338 ///
339 /// ```rust,no_run
340 /// use ridewithgps_client::RideWithGpsClient;
341 ///
342 /// let client = RideWithGpsClient::new(
343 /// "https://ridewithgps.com",
344 /// "your-api-key",
345 /// Some("your-auth-token")
346 /// );
347 ///
348 /// client.associate_poi_with_route(12345, 67890).unwrap();
349 /// ```
350 pub fn associate_poi_with_route(&self, poi_id: u64, route_id: u64) -> Result<()> {
351 let url = format!(
352 "/api/v1/points_of_interest/{}/routes/{}.json",
353 poi_id, route_id
354 );
355 let response = self
356 .client
357 .post(self.base_url.join(&url)?)
358 .headers(self.build_headers()?)
359 .send()?;
360
361 match response.status().as_u16() {
362 200 | 201 | 204 => Ok(()),
363 _ => {
364 let status = response.status();
365 let text = response.text().unwrap_or_default();
366 Err(self.error_from_status(status.as_u16(), &text))
367 }
368 }
369 }
370
371 /// Disassociate a point of interest from a route
372 ///
373 /// Note: This endpoint is only available to organization accounts.
374 ///
375 /// # Arguments
376 ///
377 /// * `poi_id` - The POI ID
378 /// * `route_id` - The route ID
379 ///
380 /// # Example
381 ///
382 /// ```rust,no_run
383 /// use ridewithgps_client::RideWithGpsClient;
384 ///
385 /// let client = RideWithGpsClient::new(
386 /// "https://ridewithgps.com",
387 /// "your-api-key",
388 /// Some("your-auth-token")
389 /// );
390 ///
391 /// client.disassociate_poi_from_route(12345, 67890).unwrap();
392 /// ```
393 pub fn disassociate_poi_from_route(&self, poi_id: u64, route_id: u64) -> Result<()> {
394 let url = format!(
395 "/api/v1/points_of_interest/{}/routes/{}.json",
396 poi_id, route_id
397 );
398 self.delete(&url)
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_poi_deserialization() {
408 let json = r#"{
409 "id": 999,
410 "name": "Coffee Shop",
411 "description": "Great coffee",
412 "latitude": 37.7749,
413 "longitude": -122.4194,
414 "poi_type": "cafe",
415 "icon": "coffee"
416 }"#;
417
418 let poi: PointOfInterest = serde_json::from_str(json).unwrap();
419 assert_eq!(poi.id, 999);
420 assert_eq!(poi.name.as_deref(), Some("Coffee Shop"));
421 assert_eq!(poi.lat, Some(37.7749));
422 assert_eq!(poi.lng, Some(-122.4194));
423 assert_eq!(poi.r#type.as_deref(), Some("cafe"));
424 }
425
426 #[test]
427 fn test_poi_request_serialization() {
428 let req = PointOfInterestRequest {
429 name: Some("Bike Shop".to_string()),
430 description: Some("Full service".to_string()),
431 latitude: Some(40.7128),
432 longitude: Some(-74.0060),
433 poi_type: Some("bike_shop".to_string()),
434 icon: Some("bicycle".to_string()),
435 address: Some("123 Main St".to_string()),
436 phone: Some("555-1234".to_string()),
437 website: Some("https://example.com".to_string()),
438 };
439
440 let json = serde_json::to_value(&req).unwrap();
441 assert_eq!(json.get("name").unwrap(), "Bike Shop");
442 assert_eq!(json.get("latitude").unwrap(), 40.7128);
443 assert_eq!(json.get("poi_type").unwrap(), "bike_shop");
444 }
445
446 #[test]
447 fn test_poi_wrapper_deserialization() {
448 let json = r#"{
449 "point_of_interest": {
450 "id": 777,
451 "name": "Wrapped POI",
452 "latitude": 40.0,
453 "longitude": -120.0,
454 "poi_type": "rest_stop"
455 }
456 }"#;
457
458 #[derive(Deserialize)]
459 struct PoiWrapper {
460 point_of_interest: PointOfInterest,
461 }
462
463 let wrapper: PoiWrapper = serde_json::from_str(json).unwrap();
464 assert_eq!(wrapper.point_of_interest.id, 777);
465 assert_eq!(
466 wrapper.point_of_interest.name.as_deref(),
467 Some("Wrapped POI")
468 );
469 assert_eq!(wrapper.point_of_interest.lat, Some(40.0));
470 assert_eq!(
471 wrapper.point_of_interest.r#type.as_deref(),
472 Some("rest_stop")
473 );
474 }
475
476 #[test]
477 fn test_poi_with_tags_and_type_info() {
478 let json = r#"{
479 "id": 444,
480 "name": "Tagged POI",
481 "poi_type": "cafe",
482 "type_id": 5,
483 "type_name": "Coffee Shop",
484 "tag_names": ["espresso", "wifi", "outdoor-seating"]
485 }"#;
486
487 let poi: PointOfInterest = serde_json::from_str(json).unwrap();
488 assert_eq!(poi.id, 444);
489 assert_eq!(poi.r#type.as_deref(), Some("cafe"));
490 assert_eq!(poi.type_id, Some(5));
491 assert_eq!(poi.type_name.as_deref(), Some("Coffee Shop"));
492 assert!(poi.tag_names.is_some());
493 let tags = poi.tag_names.unwrap();
494 assert_eq!(tags.len(), 3);
495 assert_eq!(tags[0], "espresso");
496 }
497
498 #[test]
499 fn test_poi_field_aliases() {
500 // Test that both latitude/lat and longitude/lng work
501 let json_with_full_names = r#"{
502 "id": 111,
503 "latitude": 37.5,
504 "longitude": -122.5,
505 "poi_type": "water"
506 }"#;
507
508 let poi1: PointOfInterest = serde_json::from_str(json_with_full_names).unwrap();
509 assert_eq!(poi1.lat, Some(37.5));
510 assert_eq!(poi1.lng, Some(-122.5));
511 assert_eq!(poi1.r#type.as_deref(), Some("water"));
512 }
513}