1use bytes::Bytes;
7use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
8use reqwest::{multipart, Client, Response, StatusCode};
9use serde::de::DeserializeOwned;
10use std::path::Path;
11
12use crate::client::tokens::OAuth2Token;
13use crate::error::{GarminError, Result};
14
15const API_USER_AGENT: &str = "GCM-iOS-5.7.2.1";
17
18#[derive(Clone)]
20pub struct GarminClient {
21 client: Client,
22 base_url: String,
23}
24
25impl GarminClient {
26 pub fn new() -> Self {
28 Self {
29 client: Client::builder()
30 .timeout(std::time::Duration::from_secs(30))
31 .build()
32 .expect("Failed to create HTTP client"),
33 base_url: "https://connectapi.garmin.com".to_string(),
34 }
35 }
36
37 #[doc(hidden)]
39 pub fn new_with_base_url(base_url: &str) -> Self {
40 Self {
41 client: Client::builder()
42 .timeout(std::time::Duration::from_secs(30))
43 .build()
44 .expect("Failed to create HTTP client"),
45 base_url: base_url.to_string(),
46 }
47 }
48
49 fn build_url(&self, path: &str) -> String {
51 format!("{}{}", self.base_url, path)
52 }
53
54 fn build_headers(&self, token: &OAuth2Token) -> HeaderMap {
56 let mut headers = HeaderMap::new();
57 headers.insert(USER_AGENT, HeaderValue::from_static(API_USER_AGENT));
58 headers.insert(
59 AUTHORIZATION,
60 HeaderValue::from_str(&token.authorization_header()).unwrap(),
61 );
62 headers
63 }
64
65 pub async fn get(&self, token: &OAuth2Token, path: &str) -> Result<Response> {
67 let url = self.build_url(path);
68 let headers = self.build_headers(token);
69
70 let response = self
71 .client
72 .get(&url)
73 .headers(headers)
74 .send()
75 .await
76 .map_err(GarminError::Http)?;
77
78 self.handle_response_status(response).await
79 }
80
81 pub async fn get_json<T: DeserializeOwned>(
83 &self,
84 token: &OAuth2Token,
85 path: &str,
86 ) -> Result<T> {
87 let response = self.get(token, path).await?;
88 response.json().await.map_err(|e| {
89 GarminError::invalid_response(format!("Failed to parse JSON response: {}", e))
90 })
91 }
92
93 pub async fn post_json(
95 &self,
96 token: &OAuth2Token,
97 path: &str,
98 body: &serde_json::Value,
99 ) -> Result<serde_json::Value> {
100 let url = self.build_url(path);
101 let headers = self.build_headers(token);
102
103 let response = self
104 .client
105 .post(&url)
106 .headers(headers)
107 .json(body)
108 .send()
109 .await
110 .map_err(GarminError::Http)?;
111
112 let response = self.handle_response_status(response).await?;
113 response.json().await.map_err(|e| {
114 GarminError::invalid_response(format!("Failed to parse JSON response: {}", e))
115 })
116 }
117
118 pub async fn put_json(
120 &self,
121 token: &OAuth2Token,
122 path: &str,
123 body: &serde_json::Value,
124 ) -> Result<serde_json::Value> {
125 let url = self.build_url(path);
126 let headers = self.build_headers(token);
127
128 let response = self
129 .client
130 .put(&url)
131 .headers(headers)
132 .json(body)
133 .send()
134 .await
135 .map_err(GarminError::Http)?;
136
137 let response = self.handle_response_status(response).await?;
138 let text = response.text().await.unwrap_or_default();
140 if text.trim().is_empty() {
141 return Ok(serde_json::Value::Null);
142 }
143 serde_json::from_str(&text).map_err(|e| {
144 GarminError::invalid_response(format!("Failed to parse JSON response: {}", e))
145 })
146 }
147
148 pub async fn download(&self, token: &OAuth2Token, path: &str) -> Result<Bytes> {
150 let response = self.get(token, path).await?;
151 response.bytes().await.map_err(GarminError::Http)
152 }
153
154 pub async fn upload(
156 &self,
157 token: &OAuth2Token,
158 path: &str,
159 file_path: &Path,
160 ) -> Result<serde_json::Value> {
161 let url = self.build_url(path);
162 let headers = self.build_headers(token);
163
164 let file_name = file_path
166 .file_name()
167 .and_then(|n| n.to_str())
168 .unwrap_or("activity.fit")
169 .to_string();
170
171 let file_bytes = tokio::fs::read(file_path)
172 .await
173 .map_err(|e| GarminError::invalid_response(format!("Failed to read file: {}", e)))?;
174
175 let part = multipart::Part::bytes(file_bytes)
177 .file_name(file_name)
178 .mime_str("application/octet-stream")
179 .map_err(|e| GarminError::invalid_response(format!("Invalid MIME type: {}", e)))?;
180
181 let form = multipart::Form::new().part("file", part);
182
183 let response = self
184 .client
185 .post(&url)
186 .headers(headers)
187 .multipart(form)
188 .send()
189 .await
190 .map_err(GarminError::Http)?;
191
192 let response = self.handle_response_status(response).await?;
193 response.json().await.map_err(|e| {
194 GarminError::invalid_response(format!("Failed to parse upload response: {}", e))
195 })
196 }
197
198 async fn handle_response_status(&self, response: Response) -> Result<Response> {
200 let status = response.status();
201
202 match status {
203 StatusCode::OK
204 | StatusCode::CREATED
205 | StatusCode::ACCEPTED
206 | StatusCode::NO_CONTENT => Ok(response),
207 StatusCode::UNAUTHORIZED => Err(GarminError::NotAuthenticated),
208 StatusCode::TOO_MANY_REQUESTS => Err(GarminError::RateLimited),
209 StatusCode::NOT_FOUND => {
210 let url = response.url().to_string();
211 Err(GarminError::NotFound(url))
212 }
213 _ => {
214 let status_code = status.as_u16();
215 let body = response.text().await.unwrap_or_default();
216 Err(GarminError::Api {
217 status: status_code,
218 message: body,
219 })
220 }
221 }
222 }
223}
224
225impl Default for GarminClient {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_build_url() {
237 let client = GarminClient::new();
238 assert_eq!(
239 client.build_url("/activity-service/activity/123"),
240 "https://connectapi.garmin.com/activity-service/activity/123"
241 );
242 }
243
244 #[test]
245 fn test_client_creation() {
246 let client = GarminClient::new();
247 assert_eq!(client.base_url, "https://connectapi.garmin.com");
248 }
249}