1#![deny(missing_docs)]
2use 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#[derive(Debug)]
52pub enum Error {
53 Http(reqwest::Error),
55
56 Url(url::ParseError),
58
59 Json(serde_json::Error),
61
62 ApiError(String),
64
65 AuthError(String),
67
68 NotFound(String),
70
71 BadRequest(String),
73
74 Forbidden(String),
76
77 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
117pub type Result<T> = std::result::Result<T, Error>;
119
120#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct Pagination {
123 pub record_count: Option<u64>,
125
126 pub page_count: Option<u64>,
128
129 pub page_size: Option<u64>,
131
132 pub next_page_url: Option<String>,
134}
135
136#[derive(Debug, Clone, Deserialize, Serialize)]
138pub struct PaginatedResponse<T> {
139 pub results: Vec<T>,
141
142 #[serde(flatten)]
144 pub pagination: Pagination,
145}
146
147pub struct RideWithGpsClient {
149 client: Client,
150 base_url: Url,
151 api_key: String,
152 auth_token: Option<String>,
153}
154
155impl RideWithGpsClient {
156 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 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 pub fn set_auth_token(&mut self, token: &str) {
209 self.auth_token = Some(token.to_string());
210 }
211
212 pub fn auth_token(&self) -> Option<&str> {
214 self.auth_token.as_deref()
215 }
216
217 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 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 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 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 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 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 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}