1use crate::auth::AuthConfig;
50use crate::config::Config;
51use crate::error::{NoahError, Result};
52use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
53use serde::de::DeserializeOwned;
54use url::Url;
55
56#[derive(Clone)]
79pub struct NoahClient {
80 config: Config,
81 auth_config: AuthConfig,
82 #[cfg(feature = "async")]
83 client: reqwest::Client,
84 #[cfg(feature = "sync")]
85 blocking_client: reqwest::blocking::Client,
86}
87
88impl NoahClient {
89 pub fn new(config: Config, auth_config: AuthConfig) -> Result<Self> {
132 #[cfg(feature = "async")]
133 let client = {
134 let mut headers = HeaderMap::new();
135 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
136 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
137 headers.insert(
138 "User-Agent",
139 HeaderValue::from_str(&config.user_agent)
140 .map_err(|e| NoahError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
141 );
142
143 reqwest::Client::builder()
144 .default_headers(headers)
145 .timeout(std::time::Duration::from_secs(config.timeout_secs))
146 .redirect(reqwest::redirect::Policy::limited(10))
147 .build()
148 .map_err(NoahError::HttpError)?
149 };
150
151 #[cfg(feature = "sync")]
152 let blocking_client = {
153 let mut headers = HeaderMap::new();
154 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
155 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
156 headers.insert(
157 "User-Agent",
158 HeaderValue::from_str(&config.user_agent)
159 .map_err(|e| NoahError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
160 );
161
162 reqwest::blocking::Client::builder()
163 .default_headers(headers)
164 .timeout(std::time::Duration::from_secs(config.timeout_secs))
165 .redirect(reqwest::redirect::Policy::limited(10))
166 .build()
167 .map_err(|e| {
168 NoahError::Other(anyhow::anyhow!("Failed to create blocking client: {e}"))
169 })?
170 };
171
172 Ok(Self {
173 config,
174 auth_config,
175 #[cfg(feature = "async")]
176 client,
177 #[cfg(feature = "sync")]
178 blocking_client,
179 })
180 }
181
182 pub fn base_url(&self) -> &Url {
203 &self.config.base_url
204 }
205
206 fn build_url(&self, path: &str) -> Result<Url> {
208 let path_to_join = path.strip_prefix('/').unwrap_or(path);
210
211 let mut url = self.config.base_url.clone();
212 url.path_segments_mut()
213 .map_err(|_| NoahError::Other(anyhow::anyhow!("Cannot be a base URL")))?
214 .pop_if_empty()
215 .extend(path_to_join.split('/').filter(|s| !s.is_empty()));
216
217 Ok(url)
218 }
219
220 #[cfg(feature = "async")]
221 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
223 let url = self.build_url(path)?;
224 let mut builder = self.client.get(url.as_str());
225
226 builder = crate::auth::add_auth_headers_async(
227 builder,
228 &self.auth_config,
229 "GET",
230 url.path(),
231 None,
232 )?;
233
234 let response = builder.send().await?;
235 self.handle_response(response).await
236 }
237
238 #[cfg(feature = "async")]
239 pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
241 &self,
242 path: &str,
243 body: &B,
244 ) -> Result<T> {
245 let url = self.build_url(path)?;
246 let body_bytes = serde_json::to_vec(body)?;
247 let mut builder = self.client.post(url.as_str()).body(body_bytes.clone());
248
249 builder = crate::auth::add_auth_headers_async(
250 builder,
251 &self.auth_config,
252 "POST",
253 url.path(),
254 Some(&body_bytes),
255 )?;
256
257 let response = builder.send().await?;
258 self.handle_response(response).await
259 }
260
261 #[cfg(feature = "async")]
262 pub async fn put<T: DeserializeOwned, B: serde::Serialize>(
264 &self,
265 path: &str,
266 body: &B,
267 ) -> Result<T> {
268 let url = self.build_url(path)?;
269 let body_bytes = serde_json::to_vec(body)?;
270 let mut builder = self.client.put(url.as_str()).body(body_bytes.clone());
271
272 builder = crate::auth::add_auth_headers_async(
273 builder,
274 &self.auth_config,
275 "PUT",
276 url.path(),
277 Some(&body_bytes),
278 )?;
279
280 let response = builder.send().await?;
281 self.handle_response(response).await
282 }
283
284 #[cfg(feature = "sync")]
285 pub fn get_blocking<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
287 let url = self.build_url(path)?;
288 let mut builder = self.blocking_client.get(url.as_str());
289
290 builder = crate::auth::add_auth_headers_sync(
291 builder,
292 &self.auth_config,
293 "GET",
294 url.path(),
295 None,
296 )?;
297
298 let response = builder.send()?;
299 self.handle_blocking_response(response)
300 }
301
302 #[cfg(feature = "sync")]
303 pub fn post_blocking<T: DeserializeOwned, B: serde::Serialize>(
305 &self,
306 path: &str,
307 body: &B,
308 ) -> Result<T> {
309 let url = self.build_url(path)?;
310 let body_bytes = serde_json::to_vec(body)?;
311 let mut builder = self
312 .blocking_client
313 .post(url.as_str())
314 .body(body_bytes.clone());
315
316 builder = crate::auth::add_auth_headers_sync(
317 builder,
318 &self.auth_config,
319 "POST",
320 url.path(),
321 Some(&body_bytes),
322 )?;
323
324 let response = builder.send()?;
325 self.handle_blocking_response(response)
326 }
327
328 #[cfg(feature = "sync")]
329 pub fn put_blocking<T: DeserializeOwned, B: serde::Serialize>(
331 &self,
332 path: &str,
333 body: &B,
334 ) -> Result<T> {
335 let url = self.build_url(path)?;
336 let body_bytes = serde_json::to_vec(body)?;
337 let mut builder = self
338 .blocking_client
339 .put(url.as_str())
340 .body(body_bytes.clone());
341
342 builder = crate::auth::add_auth_headers_sync(
343 builder,
344 &self.auth_config,
345 "PUT",
346 url.path(),
347 Some(&body_bytes),
348 )?;
349
350 let response = builder.send()?;
351 self.handle_blocking_response(response)
352 }
353
354 #[cfg(feature = "async")]
355 async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
356 let status = response.status();
357 let url = response.url().clone();
358 let text = response.text().await?;
359
360 if status.is_success() {
361 if text.is_empty() {
362 serde_json::from_str("null")
364 .map_err(|_| NoahError::ValidationError("Empty response body".to_string()))
365 } else {
366 serde_json::from_str(&text).map_err(NoahError::DeserializationError)
367 }
368 } else {
369 match serde_json::from_str::<crate::error::ApiErrorResponse>(&text) {
371 Ok(api_error) => Err(NoahError::ApiError(Box::new(api_error))),
372 Err(_) => Err(NoahError::Other(anyhow::anyhow!(
373 "HTTP {} from {}: {}",
374 status,
375 url,
376 if text.len() > 200 {
377 format!("{}...", &text[..200])
378 } else {
379 text
380 }
381 ))),
382 }
383 }
384 }
385
386 #[cfg(feature = "sync")]
387 fn handle_blocking_response<T: DeserializeOwned>(
388 &self,
389 response: reqwest::blocking::Response,
390 ) -> Result<T> {
391 let status = response.status();
392 let text = response.text()?;
393
394 if status.is_success() {
395 if text.is_empty() {
396 serde_json::from_str("null")
398 .map_err(|_| NoahError::ValidationError("Empty response body".to_string()))
399 } else {
400 serde_json::from_str(&text).map_err(NoahError::DeserializationError)
401 }
402 } else {
403 match serde_json::from_str::<crate::error::ApiErrorResponse>(&text) {
405 Ok(api_error) => Err(NoahError::ApiError(Box::new(api_error))),
406 Err(_) => Err(NoahError::Other(anyhow::anyhow!("HTTP {status}: {text}"))),
407 }
408 }
409 }
410}