#![deny(missing_docs)]
use log::trace;
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use std::fmt;
use url::Url;
mod auth;
mod collections;
mod events;
mod members;
mod poi;
mod routes;
mod sync;
mod trips;
mod users;
pub use auth::*;
pub use collections::*;
pub use events::*;
pub use members::*;
pub use poi::*;
pub use routes::*;
pub use sync::*;
pub use trips::*;
pub use users::*;
#[derive(Debug)]
pub enum Error {
Http(reqwest::Error),
Url(url::ParseError),
Json(serde_json::Error),
ApiError(String),
AuthError(String),
NotFound(String),
BadRequest(String),
Forbidden(String),
ValidationError(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::Http(e) => write!(f, "HTTP error: {}", e),
Error::Url(e) => write!(f, "URL error: {}", e),
Error::Json(e) => write!(f, "JSON error: {}", e),
Error::ApiError(s) => write!(f, "API error: {}", s),
Error::AuthError(s) => write!(f, "Authentication error: {}", s),
Error::NotFound(s) => write!(f, "Resource not found: {}", s),
Error::BadRequest(s) => write!(f, "Bad request: {}", s),
Error::Forbidden(s) => write!(f, "Forbidden: {}", s),
Error::ValidationError(s) => write!(f, "Validation error: {}", s),
}
}
}
impl std::error::Error for Error {}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Http(e)
}
}
impl From<url::ParseError> for Error {
fn from(e: url::ParseError) -> Self {
Error::Url(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Json(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Pagination {
pub record_count: Option<u64>,
pub page_count: Option<u64>,
pub page_size: Option<u64>,
pub next_page_url: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PaginatedResponse<T> {
pub results: Vec<T>,
#[serde(flatten)]
pub pagination: Pagination,
}
pub struct RideWithGpsClient {
client: Client,
base_url: Url,
api_key: String,
auth_token: Option<String>,
}
impl RideWithGpsClient {
pub fn new(base_url: &str, api_key: &str, auth_token: Option<&str>) -> Self {
Self {
client: Client::new(),
base_url: Url::parse(base_url).expect("Invalid base URL"),
api_key: api_key.to_string(),
auth_token: auth_token.map(|s| s.to_string()),
}
}
pub fn with_credentials(
base_url: &str,
api_key: &str,
email: &str,
password: &str,
) -> Result<Self> {
let mut client = Self::new(base_url, api_key, None);
let auth_token = client.create_auth_token(email, password)?;
client.auth_token = Some(auth_token.auth_token);
Ok(client)
}
pub fn set_auth_token(&mut self, token: &str) {
self.auth_token = Some(token.to_string());
}
pub fn auth_token(&self) -> Option<&str> {
self.auth_token.as_deref()
}
fn build_headers(&self) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
headers.insert(
"x-rwgps-api-key",
HeaderValue::from_str(&self.api_key)
.map_err(|e| Error::AuthError(format!("Invalid API key format: {}", e)))?,
);
if let Some(token) = &self.auth_token {
headers.insert(
"x-rwgps-auth-token",
HeaderValue::from_str(token)
.map_err(|e| Error::AuthError(format!("Invalid auth token format: {}", e)))?,
);
}
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
Ok(headers)
}
fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T> {
let url = self.base_url.join(path)?;
trace!("GET {}", url);
let headers = self.build_headers()?;
let response = self.client.get(url).headers(headers).send()?;
self.handle_response(response)
}
fn post<T: for<'de> Deserialize<'de>, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
let url = self.base_url.join(path)?;
trace!("POST {}", url);
let headers = self.build_headers()?;
let response = self.client.post(url).headers(headers).json(body).send()?;
self.handle_response(response)
}
fn put<T: for<'de> Deserialize<'de>, B: Serialize>(&self, path: &str, body: &B) -> Result<T> {
let url = self.base_url.join(path)?;
trace!("PUT {}", url);
let headers = self.build_headers()?;
let response = self.client.put(url).headers(headers).json(body).send()?;
self.handle_response(response)
}
fn delete(&self, path: &str) -> Result<()> {
let url = self.base_url.join(path)?;
trace!("DELETE {}", url);
let headers = self.build_headers()?;
let response = self.client.delete(url).headers(headers).send()?;
match response.status().as_u16() {
204 => Ok(()),
_ => {
let status = response.status();
let text = response.text().unwrap_or_default();
Err(self.error_from_status(status.as_u16(), &text))
}
}
}
fn handle_response<T: for<'de> Deserialize<'de>>(
&self,
response: reqwest::blocking::Response,
) -> Result<T> {
let status = response.status();
match status.as_u16() {
200 | 201 => {
let text = response.text()?;
serde_json::from_str(&text).map_err(Error::Json)
}
_ => {
let text = response.text().unwrap_or_default();
Err(self.error_from_status(status.as_u16(), &text))
}
}
}
fn error_from_status(&self, status: u16, body: &str) -> Error {
match status {
400 => Error::BadRequest(body.to_string()),
401 => Error::AuthError(body.to_string()),
403 => Error::Forbidden(body.to_string()),
404 => Error::NotFound(body.to_string()),
422 => Error::ValidationError(body.to_string()),
_ => Error::ApiError(format!("HTTP {}: {}", status, body)),
}
}
}
impl fmt::Debug for RideWithGpsClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RideWithGpsClient")
.field("base_url", &self.base_url.as_str())
.field("api_key", &"***")
.field("auth_token", &self.auth_token.as_ref().map(|_| "***"))
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = RideWithGpsClient::new(
"https://ridewithgps.com",
"test-api-key",
Some("test-token"),
);
assert_eq!(client.base_url.as_str(), "https://ridewithgps.com/");
assert_eq!(client.api_key, "test-api-key");
assert_eq!(client.auth_token.as_deref(), Some("test-token"));
}
#[test]
fn test_set_auth_token() {
let mut client = RideWithGpsClient::new("https://ridewithgps.com", "test-api-key", None);
assert_eq!(client.auth_token(), None);
client.set_auth_token("new-token");
assert_eq!(client.auth_token(), Some("new-token"));
}
}