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