ridewithgps_client/
lib.rs

1#![deny(missing_docs)]
2//! A Rust client for the RideWithGPS API.
3//!
4//! This crate provides a client for interacting with the RideWithGPS API v1,
5//! allowing you to manage routes, trips, events, collections, and more.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use ridewithgps_client::RideWithGpsClient;
11//!
12//! let client = RideWithGpsClient::new(
13//!     "https://ridewithgps.com",
14//!     "your-api-key",
15//!     Some("your-auth-token")
16//! );
17//!
18//! // Get current user
19//! let user = client.get_current_user().unwrap();
20//! println!("User: {:?}", user);
21//! ```
22
23use log::trace;
24use reqwest::blocking::Client;
25use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
26use serde::{Deserialize, Serialize};
27use std::fmt;
28use url::Url;
29
30mod auth;
31mod collections;
32mod events;
33mod members;
34mod poi;
35mod routes;
36mod sync;
37mod trips;
38mod users;
39
40pub use auth::*;
41pub use collections::*;
42pub use events::*;
43pub use members::*;
44pub use poi::*;
45pub use routes::*;
46pub use sync::*;
47pub use trips::*;
48pub use users::*;
49
50/// Error type for RideWithGPS API operations
51#[derive(Debug)]
52pub enum Error {
53    /// HTTP request error
54    Http(reqwest::Error),
55
56    /// URL parsing error
57    Url(url::ParseError),
58
59    /// JSON serialization/deserialization error
60    Json(serde_json::Error),
61
62    /// API error response
63    ApiError(String),
64
65    /// Authentication error
66    AuthError(String),
67
68    /// Resource not found
69    NotFound(String),
70
71    /// Bad request
72    BadRequest(String),
73
74    /// Forbidden
75    Forbidden(String),
76
77    /// Validation error
78    ValidationError(String),
79}
80
81impl std::fmt::Display for Error {
82    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
83        match self {
84            Error::Http(e) => write!(f, "HTTP error: {}", e),
85            Error::Url(e) => write!(f, "URL error: {}", e),
86            Error::Json(e) => write!(f, "JSON error: {}", e),
87            Error::ApiError(s) => write!(f, "API error: {}", s),
88            Error::AuthError(s) => write!(f, "Authentication error: {}", s),
89            Error::NotFound(s) => write!(f, "Resource not found: {}", s),
90            Error::BadRequest(s) => write!(f, "Bad request: {}", s),
91            Error::Forbidden(s) => write!(f, "Forbidden: {}", s),
92            Error::ValidationError(s) => write!(f, "Validation error: {}", s),
93        }
94    }
95}
96
97impl std::error::Error for Error {}
98
99impl From<reqwest::Error> for Error {
100    fn from(e: reqwest::Error) -> Self {
101        Error::Http(e)
102    }
103}
104
105impl From<url::ParseError> for Error {
106    fn from(e: url::ParseError) -> Self {
107        Error::Url(e)
108    }
109}
110
111impl From<serde_json::Error> for Error {
112    fn from(e: serde_json::Error) -> Self {
113        Error::Json(e)
114    }
115}
116
117/// Result type for RideWithGPS API operations
118pub type Result<T> = std::result::Result<T, Error>;
119
120/// Pagination information for list responses
121#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct Pagination {
123    /// Total number of records
124    pub record_count: Option<u64>,
125
126    /// Total number of pages
127    pub page_count: Option<u64>,
128
129    /// Current page size
130    pub page_size: Option<u64>,
131
132    /// URL for the next page
133    pub next_page_url: Option<String>,
134}
135
136/// Common response wrapper for paginated lists
137#[derive(Debug, Clone, Deserialize, Serialize)]
138pub struct PaginatedResponse<T> {
139    /// The result items
140    pub results: Vec<T>,
141
142    /// Pagination information
143    #[serde(flatten)]
144    pub pagination: Pagination,
145}
146
147/// Main client for the RideWithGPS API
148pub struct RideWithGpsClient {
149    client: Client,
150    base_url: Url,
151    api_key: String,
152    auth_token: Option<String>,
153}
154
155impl RideWithGpsClient {
156    /// Create a new RideWithGPS API client
157    ///
158    /// # Arguments
159    ///
160    /// * `base_url` - The base URL for the API (e.g., "https://ridewithgps.com")
161    /// * `api_key` - Your API key
162    /// * `auth_token` - Optional authentication token for user-specific operations
163    ///
164    /// # Example
165    ///
166    /// ```rust,no_run
167    /// use ridewithgps_client::RideWithGpsClient;
168    ///
169    /// let client = RideWithGpsClient::new(
170    ///     "https://ridewithgps.com",
171    ///     "your-api-key",
172    ///     None
173    /// );
174    /// ```
175    pub fn new(base_url: &str, api_key: &str, auth_token: Option<&str>) -> Self {
176        Self {
177            client: Client::new(),
178            base_url: Url::parse(base_url).expect("Invalid base URL"),
179            api_key: api_key.to_string(),
180            auth_token: auth_token.map(|s| s.to_string()),
181        }
182    }
183
184    /// Create a new client with authentication credentials
185    ///
186    /// This will create a client and authenticate using email and password
187    /// to obtain an auth token.
188    ///
189    /// # Arguments
190    ///
191    /// * `base_url` - The base URL for the API
192    /// * `api_key` - Your API key
193    /// * `email` - User email
194    /// * `password` - User password
195    pub fn with_credentials(
196        base_url: &str,
197        api_key: &str,
198        email: &str,
199        password: &str,
200    ) -> Result<Self> {
201        let mut client = Self::new(base_url, api_key, None);
202        let auth_token = client.create_auth_token(email, password)?;
203        client.auth_token = Some(auth_token.auth_token);
204        Ok(client)
205    }
206
207    /// Set the authentication token
208    pub fn set_auth_token(&mut self, token: &str) {
209        self.auth_token = Some(token.to_string());
210    }
211
212    /// Get the authentication token
213    pub fn auth_token(&self) -> Option<&str> {
214        self.auth_token.as_deref()
215    }
216
217    /// Build headers for API requests
218    fn build_headers(&self) -> Result<HeaderMap> {
219        let mut headers = HeaderMap::new();
220        headers.insert(
221            "x-rwgps-api-key",
222            HeaderValue::from_str(&self.api_key)
223                .map_err(|e| Error::AuthError(format!("Invalid API key format: {}", e)))?,
224        );
225
226        if let Some(token) = &self.auth_token {
227            headers.insert(
228                "x-rwgps-auth-token",
229                HeaderValue::from_str(token)
230                    .map_err(|e| Error::AuthError(format!("Invalid auth token format: {}", e)))?,
231            );
232        }
233
234        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
235
236        Ok(headers)
237    }
238
239    /// Execute a GET request
240    fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T> {
241        let url = self.base_url.join(path)?;
242        trace!("GET {}", url);
243
244        let headers = self.build_headers()?;
245        let response = self.client.get(url).headers(headers).send()?;
246
247        self.handle_response(response)
248    }
249
250    /// Execute a POST request
251    fn post<T: for<'de> Deserialize<'de>, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
252        let url = self.base_url.join(path)?;
253        trace!("POST {}", url);
254
255        let headers = self.build_headers()?;
256        let response = self.client.post(url).headers(headers).json(body).send()?;
257
258        self.handle_response(response)
259    }
260
261    /// Execute a PUT request
262    fn put<T: for<'de> Deserialize<'de>, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
263        let url = self.base_url.join(path)?;
264        trace!("PUT {}", url);
265
266        let headers = self.build_headers()?;
267        let response = self.client.put(url).headers(headers).json(body).send()?;
268
269        self.handle_response(response)
270    }
271
272    /// Execute a DELETE request
273    fn delete(&self, path: &str) -> Result<()> {
274        let url = self.base_url.join(path)?;
275        trace!("DELETE {}", url);
276
277        let headers = self.build_headers()?;
278        let response = self.client.delete(url).headers(headers).send()?;
279
280        match response.status().as_u16() {
281            204 => Ok(()),
282            _ => {
283                let status = response.status();
284                let text = response.text().unwrap_or_default();
285                Err(self.error_from_status(status.as_u16(), &text))
286            }
287        }
288    }
289
290    /// Handle API response and convert to typed result
291    fn handle_response<T: for<'de> Deserialize<'de>>(
292        &self,
293        response: reqwest::blocking::Response,
294    ) -> Result<T> {
295        let status = response.status();
296
297        match status.as_u16() {
298            200 | 201 => {
299                let text = response.text()?;
300                serde_json::from_str(&text).map_err(Error::Json)
301            }
302            _ => {
303                let text = response.text().unwrap_or_default();
304                Err(self.error_from_status(status.as_u16(), &text))
305            }
306        }
307    }
308
309    /// Convert HTTP status code to Error
310    fn error_from_status(&self, status: u16, body: &str) -> Error {
311        match status {
312            400 => Error::BadRequest(body.to_string()),
313            401 => Error::AuthError(body.to_string()),
314            403 => Error::Forbidden(body.to_string()),
315            404 => Error::NotFound(body.to_string()),
316            422 => Error::ValidationError(body.to_string()),
317            _ => Error::ApiError(format!("HTTP {}: {}", status, body)),
318        }
319    }
320}
321
322impl fmt::Debug for RideWithGpsClient {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        f.debug_struct("RideWithGpsClient")
325            .field("base_url", &self.base_url.as_str())
326            .field("api_key", &"***")
327            .field("auth_token", &self.auth_token.as_ref().map(|_| "***"))
328            .finish()
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_client_creation() {
338        let client = RideWithGpsClient::new(
339            "https://ridewithgps.com",
340            "test-api-key",
341            Some("test-token"),
342        );
343
344        assert_eq!(client.base_url.as_str(), "https://ridewithgps.com/");
345        assert_eq!(client.api_key, "test-api-key");
346        assert_eq!(client.auth_token.as_deref(), Some("test-token"));
347    }
348
349    #[test]
350    fn test_set_auth_token() {
351        let mut client = RideWithGpsClient::new("https://ridewithgps.com", "test-api-key", None);
352
353        assert_eq!(client.auth_token(), None);
354
355        client.set_auth_token("new-token");
356        assert_eq!(client.auth_token(), Some("new-token"));
357    }
358}