use bytes::Bytes;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
use reqwest::{multipart, Client, Response, StatusCode};
use serde::de::DeserializeOwned;
use std::path::Path;
use crate::client::tokens::OAuth2Token;
use crate::error::{GarminError, Result};
const API_USER_AGENT: &str = "GCM-iOS-5.7.2.1";
#[derive(Clone)]
pub struct GarminClient {
client: Client,
base_url: String,
}
impl GarminClient {
pub fn new() -> Self {
Self {
client: Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client"),
base_url: "https://connectapi.garmin.com".to_string(),
}
}
#[doc(hidden)]
pub fn new_with_base_url(base_url: &str) -> Self {
Self {
client: Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client"),
base_url: base_url.to_string(),
}
}
fn build_url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
fn build_headers(&self, token: &OAuth2Token) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(API_USER_AGENT));
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&token.authorization_header()).unwrap(),
);
headers
}
pub async fn get(&self, token: &OAuth2Token, path: &str) -> Result<Response> {
let url = self.build_url(path);
let headers = self.build_headers(token);
let response = self
.client
.get(&url)
.headers(headers)
.send()
.await
.map_err(GarminError::Http)?;
self.handle_response_status(response).await
}
pub async fn get_json<T: DeserializeOwned>(
&self,
token: &OAuth2Token,
path: &str,
) -> Result<T> {
let response = self.get(token, path).await?;
response.json().await.map_err(|e| {
GarminError::invalid_response(format!("Failed to parse JSON response: {}", e))
})
}
pub async fn post_json(
&self,
token: &OAuth2Token,
path: &str,
body: &serde_json::Value,
) -> Result<serde_json::Value> {
let url = self.build_url(path);
let headers = self.build_headers(token);
let response = self
.client
.post(&url)
.headers(headers)
.json(body)
.send()
.await
.map_err(GarminError::Http)?;
let response = self.handle_response_status(response).await?;
response.json().await.map_err(|e| {
GarminError::invalid_response(format!("Failed to parse JSON response: {}", e))
})
}
pub async fn download(&self, token: &OAuth2Token, path: &str) -> Result<Bytes> {
let response = self.get(token, path).await?;
response.bytes().await.map_err(GarminError::Http)
}
pub async fn upload(
&self,
token: &OAuth2Token,
path: &str,
file_path: &Path,
) -> Result<serde_json::Value> {
let url = self.build_url(path);
let headers = self.build_headers(token);
let file_name = file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("activity.fit")
.to_string();
let file_bytes = tokio::fs::read(file_path)
.await
.map_err(|e| GarminError::invalid_response(format!("Failed to read file: {}", e)))?;
let part = multipart::Part::bytes(file_bytes)
.file_name(file_name)
.mime_str("application/octet-stream")
.map_err(|e| GarminError::invalid_response(format!("Invalid MIME type: {}", e)))?;
let form = multipart::Form::new().part("file", part);
let response = self
.client
.post(&url)
.headers(headers)
.multipart(form)
.send()
.await
.map_err(GarminError::Http)?;
let response = self.handle_response_status(response).await?;
response.json().await.map_err(|e| {
GarminError::invalid_response(format!("Failed to parse upload response: {}", e))
})
}
async fn handle_response_status(&self, response: Response) -> Result<Response> {
let status = response.status();
match status {
StatusCode::OK
| StatusCode::CREATED
| StatusCode::ACCEPTED
| StatusCode::NO_CONTENT => Ok(response),
StatusCode::UNAUTHORIZED => Err(GarminError::NotAuthenticated),
StatusCode::TOO_MANY_REQUESTS => Err(GarminError::RateLimited),
StatusCode::NOT_FOUND => {
let url = response.url().to_string();
Err(GarminError::NotFound(url))
}
_ => {
let status_code = status.as_u16();
let body = response.text().await.unwrap_or_default();
Err(GarminError::Api {
status: status_code,
message: body,
})
}
}
}
}
impl Default for GarminClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_url() {
let client = GarminClient::new();
assert_eq!(
client.build_url("/activity-service/activity/123"),
"https://connectapi.garmin.com/activity-service/activity/123"
);
}
#[test]
fn test_client_creation() {
let client = GarminClient::new();
assert_eq!(client.base_url, "https://connectapi.garmin.com");
}
}