1use crate::auth::AuthConfig;
48use crate::config::Config;
49use crate::error::{RainError, Result};
50use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
51use serde::de::DeserializeOwned;
52use url::Url;
53
54#[derive(Clone)]
77pub struct RainClient {
78 config: Config,
79 auth_config: AuthConfig,
80 #[cfg(feature = "async")]
81 client: reqwest::Client,
82 #[cfg(feature = "sync")]
83 blocking_client: reqwest::blocking::Client,
84}
85
86impl RainClient {
87 pub fn new(config: Config, auth_config: AuthConfig) -> Result<Self> {
118 #[cfg(feature = "async")]
119 let client = {
120 let mut headers = HeaderMap::new();
121 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
122 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
123 headers.insert(
124 "User-Agent",
125 HeaderValue::from_str(&config.user_agent)
126 .map_err(|e| RainError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
127 );
128
129 reqwest::Client::builder()
130 .default_headers(headers)
131 .timeout(std::time::Duration::from_secs(config.timeout_secs))
132 .redirect(reqwest::redirect::Policy::limited(10))
133 .build()
134 .map_err(RainError::HttpError)?
135 };
136
137 #[cfg(feature = "sync")]
138 let blocking_client = {
139 let mut headers = HeaderMap::new();
140 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
141 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
142 headers.insert(
143 "User-Agent",
144 HeaderValue::from_str(&config.user_agent)
145 .map_err(|e| RainError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
146 );
147
148 reqwest::blocking::Client::builder()
149 .default_headers(headers)
150 .timeout(std::time::Duration::from_secs(config.timeout_secs))
151 .redirect(reqwest::redirect::Policy::limited(10))
152 .build()
153 .map_err(|e| {
154 RainError::Other(anyhow::anyhow!("Failed to create blocking client: {e}"))
155 })?
156 };
157
158 Ok(Self {
159 config,
160 auth_config,
161 #[cfg(feature = "async")]
162 client,
163 #[cfg(feature = "sync")]
164 blocking_client,
165 })
166 }
167
168 pub fn base_url(&self) -> &Url {
189 &self.config.base_url
190 }
191
192 fn build_url(&self, path: &str) -> Result<Url> {
194 let path_to_join = path.strip_prefix('/').unwrap_or(path);
196
197 let mut url = self.config.base_url.clone();
198 url.path_segments_mut()
199 .map_err(|_| RainError::Other(anyhow::anyhow!("Cannot be a base URL")))?
200 .pop_if_empty()
201 .extend(path_to_join.split('/').filter(|s| !s.is_empty()));
202
203 Ok(url)
204 }
205
206 #[cfg(feature = "async")]
207 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
209 let url = self.build_url(path)?;
210 let builder = self.client.get(url.as_str());
211 let builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
212
213 let response = builder.send().await?;
214 self.handle_response(response).await
215 }
216
217 #[cfg(feature = "async")]
218 pub async fn get_bytes(&self, path: &str) -> Result<Vec<u8>> {
220 let url = self.build_url(path)?;
221 let builder = self.client.get(url.as_str());
222 let builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
223
224 let response = builder.send().await?;
225 let status = response.status();
226 if status.is_success() {
227 let bytes = response.bytes().await?;
228 Ok(bytes.to_vec())
229 } else {
230 let text = response.text().await?;
231 Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}")))
232 }
233 }
234
235 #[cfg(feature = "async")]
236 pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
238 &self,
239 path: &str,
240 body: &B,
241 ) -> Result<T> {
242 let url = self.build_url(path)?;
243 let body_bytes = serde_json::to_vec(body)?;
244 let builder = self.client.post(url.as_str()).body(body_bytes.clone());
245 let builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
246
247 let response = builder.send().await?;
248 self.handle_response(response).await
249 }
250
251 #[cfg(feature = "async")]
252 pub async fn patch<T: DeserializeOwned, B: serde::Serialize>(
254 &self,
255 path: &str,
256 body: &B,
257 ) -> Result<T> {
258 let url = self.build_url(path)?;
259 let body_bytes = serde_json::to_vec(body)?;
260 let builder = self.client.patch(url.as_str()).body(body_bytes.clone());
261 let builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
262
263 let response = builder.send().await?;
264 self.handle_response(response).await
265 }
266
267 #[cfg(feature = "async")]
268 pub async fn put<T: DeserializeOwned, B: serde::Serialize>(
270 &self,
271 path: &str,
272 body: &B,
273 ) -> Result<T> {
274 let url = self.build_url(path)?;
275 let body_bytes = serde_json::to_vec(body)?;
276 let builder = self.client.put(url.as_str()).body(body_bytes.clone());
277 let builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
278
279 let response = builder.send().await?;
280 self.handle_response(response).await
281 }
282
283 #[cfg(feature = "async")]
284 pub async fn get_with_headers<T: DeserializeOwned>(
286 &self,
287 path: &str,
288 headers: Vec<(&str, &str)>,
289 ) -> Result<T> {
290 let url = self.build_url(path)?;
291 let mut builder = self.client.get(url.as_str());
292 builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
293
294 for (key, value) in headers {
295 builder = builder.header(key, value);
296 }
297
298 let response = builder.send().await?;
299 self.handle_response(response).await
300 }
301
302 #[cfg(feature = "async")]
303 pub async fn put_with_headers<T: DeserializeOwned, B: serde::Serialize>(
305 &self,
306 path: &str,
307 body: &B,
308 headers: Vec<(&str, &str)>,
309 ) -> Result<T> {
310 let url = self.build_url(path)?;
311 let body_bytes = serde_json::to_vec(body)?;
312 let mut builder = self.client.put(url.as_str()).body(body_bytes.clone());
313 builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
314
315 for (key, value) in headers {
316 builder = builder.header(key, value);
317 }
318
319 let response = builder.send().await?;
320 self.handle_response(response).await
321 }
322
323 #[cfg(feature = "async")]
324 pub async fn delete(&self, path: &str) -> Result<()> {
326 let url = self.build_url(path)?;
327 let builder = self.client.delete(url.as_str());
328 let builder = crate::auth::add_auth_headers_async(builder, &self.auth_config);
329
330 let response = builder.send().await?;
331 let status = response.status();
332 if status.is_success() || status == reqwest::StatusCode::NO_CONTENT {
333 Ok(())
334 } else {
335 let text = response.text().await?;
336 Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}")))
337 }
338 }
339
340 #[cfg(feature = "async")]
341 pub async fn put_multipart<T: DeserializeOwned>(
343 &self,
344 path: &str,
345 form: reqwest::multipart::Form,
346 ) -> Result<T> {
347 let url = self.build_url(path)?;
348 let mut headers = HeaderMap::new();
349 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
351 headers.insert(
352 "User-Agent",
353 HeaderValue::from_str(&self.config.user_agent)
354 .map_err(|e| RainError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
355 );
356
357 let request = self
358 .client
359 .put(url.as_str())
360 .headers(headers)
361 .header("Api-Key", &self.auth_config.api_key)
362 .multipart(form);
363
364 let response = request.send().await?;
365 self.handle_response(response).await
366 }
367
368 #[cfg(feature = "async")]
369 pub async fn put_multipart_no_content(
371 &self,
372 path: &str,
373 form: reqwest::multipart::Form,
374 ) -> Result<()> {
375 let url = self.build_url(path)?;
376 let mut headers = HeaderMap::new();
377 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
379 headers.insert(
380 "User-Agent",
381 HeaderValue::from_str(&self.config.user_agent)
382 .map_err(|e| RainError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
383 );
384
385 let request = self
386 .client
387 .put(url.as_str())
388 .headers(headers)
389 .header("Api-Key", &self.auth_config.api_key)
390 .multipart(form);
391
392 let response = request.send().await?;
393 let status = response.status();
394 if status == reqwest::StatusCode::NO_CONTENT || status.is_success() {
395 Ok(())
396 } else {
397 let text = response.text().await?;
398 Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}")))
399 }
400 }
401
402 #[cfg(feature = "sync")]
403 pub fn get_blocking<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
405 let url = self.build_url(path)?;
406 let builder = self.blocking_client.get(url.as_str());
407 let builder = crate::auth::add_auth_headers_sync(builder, &self.auth_config);
408
409 let response = builder.send()?;
410 self.handle_blocking_response(response)
411 }
412
413 #[cfg(feature = "sync")]
414 pub fn get_bytes_blocking(&self, path: &str) -> Result<Vec<u8>> {
416 let url = self.build_url(path)?;
417 let builder = self.blocking_client.get(url.as_str());
418 let builder = crate::auth::add_auth_headers_sync(builder, &self.auth_config);
419
420 let response = builder.send()?;
421 let status = response.status();
422 if status.is_success() {
423 let bytes = response.bytes()?;
424 Ok(bytes.to_vec())
425 } else {
426 let text = response.text()?;
427 Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}")))
428 }
429 }
430
431 #[cfg(feature = "sync")]
432 pub fn post_blocking<T: DeserializeOwned, B: serde::Serialize>(
434 &self,
435 path: &str,
436 body: &B,
437 ) -> Result<T> {
438 let url = self.build_url(path)?;
439 let body_bytes = serde_json::to_vec(body)?;
440 let builder = self
441 .blocking_client
442 .post(url.as_str())
443 .body(body_bytes.clone());
444 let builder = crate::auth::add_auth_headers_sync(builder, &self.auth_config);
445
446 let response = builder.send()?;
447 self.handle_blocking_response(response)
448 }
449
450 #[cfg(feature = "sync")]
451 pub fn patch_blocking<T: DeserializeOwned, B: serde::Serialize>(
453 &self,
454 path: &str,
455 body: &B,
456 ) -> Result<T> {
457 let url = self.build_url(path)?;
458 let body_bytes = serde_json::to_vec(body)?;
459 let builder = self
460 .blocking_client
461 .patch(url.as_str())
462 .body(body_bytes.clone());
463 let builder = crate::auth::add_auth_headers_sync(builder, &self.auth_config);
464
465 let response = builder.send()?;
466 self.handle_blocking_response(response)
467 }
468
469 #[cfg(feature = "sync")]
470 pub fn put_blocking<T: DeserializeOwned, B: serde::Serialize>(
472 &self,
473 path: &str,
474 body: &B,
475 ) -> Result<T> {
476 let url = self.build_url(path)?;
477 let body_bytes = serde_json::to_vec(body)?;
478 let builder = self
479 .blocking_client
480 .put(url.as_str())
481 .body(body_bytes.clone());
482 let builder = crate::auth::add_auth_headers_sync(builder, &self.auth_config);
483
484 let response = builder.send()?;
485 self.handle_blocking_response(response)
486 }
487
488 #[cfg(feature = "sync")]
489 pub fn put_multipart_blocking_no_content(
491 &self,
492 path: &str,
493 form: reqwest::blocking::multipart::Form,
494 ) -> Result<()> {
495 let url = self.build_url(path)?;
496 use reqwest::blocking::header::{HeaderMap, HeaderValue, ACCEPT};
497 let mut headers = HeaderMap::new();
498 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
499 headers.insert(
500 "User-Agent",
501 HeaderValue::from_str(&self.config.user_agent)
502 .map_err(|e| RainError::Other(anyhow::anyhow!("Invalid user agent: {e}")))?,
503 );
504
505 let response = self
506 .blocking_client
507 .put(url.as_str())
508 .headers(headers)
509 .header("Api-Key", &self.auth_config.api_key)
510 .multipart(form)
511 .send()?;
512
513 let status = response.status();
514 if status == reqwest::StatusCode::NO_CONTENT || status.is_success() {
515 Ok(())
516 } else {
517 let text = response.text()?;
518 Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}")))
519 }
520 }
521
522 #[cfg(feature = "sync")]
523 pub fn delete_blocking(&self, path: &str) -> Result<()> {
525 let url = self.build_url(path)?;
526 let builder = self.blocking_client.delete(url.as_str());
527 let builder = crate::auth::add_auth_headers_sync(builder, &self.auth_config);
528
529 let response = builder.send()?;
530 let status = response.status();
531 if status.is_success() || status == reqwest::StatusCode::NO_CONTENT {
532 Ok(())
533 } else {
534 let text = response.text()?;
535 Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}")))
536 }
537 }
538
539 #[cfg(feature = "async")]
540 async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
541 let status = response.status();
542 let url = response.url().clone();
543 let text = response.text().await?;
544
545 if status == reqwest::StatusCode::ACCEPTED {
547 if text.is_empty() {
548 serde_json::from_str("{}")
550 .or_else(|_| serde_json::from_str("null"))
551 .map_err(|_| RainError::ValidationError("Empty response body".to_string()))
552 } else {
553 serde_json::from_str(&text).map_err(RainError::DeserializationError)
554 }
555 } else if status.is_success() {
556 if text.is_empty() {
557 serde_json::from_str("null")
559 .map_err(|_| RainError::ValidationError("Empty response body".to_string()))
560 } else {
561 serde_json::from_str(&text).map_err(RainError::DeserializationError)
562 }
563 } else {
564 match serde_json::from_str::<crate::error::ApiErrorResponse>(&text) {
566 Ok(api_error) => Err(RainError::ApiError {
567 status: status.as_u16(),
568 response: Box::new(api_error),
569 }),
570 Err(_) => Err(RainError::Other(anyhow::anyhow!(
571 "HTTP {} from {}: {}",
572 status,
573 url,
574 if text.len() > 200 {
575 format!("{}...", &text[..200])
576 } else {
577 text
578 }
579 ))),
580 }
581 }
582 }
583
584 #[cfg(feature = "sync")]
585 fn handle_blocking_response<T: DeserializeOwned>(
586 &self,
587 response: reqwest::blocking::Response,
588 ) -> Result<T> {
589 let status = response.status();
590 let text = response.text()?;
591
592 if status == reqwest::StatusCode::ACCEPTED {
594 if text.is_empty() {
595 serde_json::from_str("{}")
597 .or_else(|_| serde_json::from_str("null"))
598 .map_err(|_| RainError::ValidationError("Empty response body".to_string()))
599 } else {
600 serde_json::from_str(&text).map_err(RainError::DeserializationError)
601 }
602 } else if status.is_success() {
603 if text.is_empty() {
604 serde_json::from_str("null")
606 .map_err(|_| RainError::ValidationError("Empty response body".to_string()))
607 } else {
608 serde_json::from_str(&text).map_err(RainError::DeserializationError)
609 }
610 } else {
611 match serde_json::from_str::<crate::error::ApiErrorResponse>(&text) {
613 Ok(api_error) => Err(RainError::ApiError {
614 status: status.as_u16(),
615 response: Box::new(api_error),
616 }),
617 Err(_) => Err(RainError::Other(anyhow::anyhow!("HTTP {status}: {text}"))),
618 }
619 }
620 }
621}