use anyhow::Result; use async_trait::async_trait; use reqwest::{ header::{HeaderMap, HeaderName, HeaderValue}, Client, Response, StatusCode, }; use serde_json::Value; use std::fmt; use std::time::Duration; pub struct ApiClient { client: Client, host: String, auth: AuthMethod, default_headers: HeaderMap, timeout: Duration, } pub enum AuthMethod { BearerToken(String), ApiKey { header_name: String, key: String, }, #[allow(dead_code)] OAuth(OAuthConfig), Custom(Box), } pub struct OAuthConfig { pub host: String, pub client_id: String, pub redirect_url: String, pub scopes: Vec, } #[async_trait] pub trait AuthProvider: Send + Sync { async fn get_auth_header(&self) -> Result<(String, String)>; } pub struct ApiResponse { pub status: StatusCode, pub payload: Option, } impl fmt::Debug for AuthMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AuthMethod::BearerToken(_) => f.debug_tuple("BearerToken").field(&"[hidden]").finish(), AuthMethod::ApiKey { header_name, .. } => f .debug_struct("ApiKey") .field("header_name", header_name) .field("key", &"[hidden]") .finish(), AuthMethod::OAuth(_) => f.debug_tuple("OAuth").field(&"[config]").finish(), AuthMethod::Custom(_) => f.debug_tuple("Custom").field(&"[provider]").finish(), } } } impl ApiResponse { pub async fn from_response(response: Response) -> Result { let status = response.status(); let payload = response.json().await.ok(); Ok(Self { status, payload }) } } pub struct ApiRequestBuilder<'a> { client: &'a ApiClient, path: &'a str, headers: HeaderMap, } impl ApiClient { pub fn new(host: String, auth: AuthMethod) -> Result { Self::with_timeout(host, auth, Duration::from_secs(600)) } pub fn with_timeout(host: String, auth: AuthMethod, timeout: Duration) -> Result { Ok(Self { client: Client::builder().timeout(timeout).build()?, host, auth, default_headers: HeaderMap::new(), timeout, }) } pub fn with_headers(mut self, headers: HeaderMap) -> Result { self.default_headers = headers; self.client = Client::builder() .timeout(self.timeout) .default_headers(self.default_headers.clone()) .build()?; Ok(self) } pub fn with_header(mut self, key: &str, value: &str) -> Result { let header_name = HeaderName::from_bytes(key.as_bytes())?; let header_value = HeaderValue::from_str(value)?; self.default_headers.insert(header_name, header_value); self.client = Client::builder() .timeout(self.timeout) .default_headers(self.default_headers.clone()) .build()?; Ok(self) } pub fn request<'a>(&'a self, path: &'a str) -> ApiRequestBuilder<'a> { ApiRequestBuilder { client: self, path, headers: HeaderMap::new(), } } pub async fn api_post(&self, path: &str, payload: &Value) -> Result { self.request(path).api_post(payload).await } pub async fn response_post(&self, path: &str, payload: &Value) -> Result { self.request(path).response_post(payload).await } pub async fn api_get(&self, path: &str) -> Result { self.request(path).api_get().await } pub async fn response_get(&self, path: &str) -> Result { self.request(path).response_get().await } fn build_url(&self, path: &str) -> Result { use url::Url; let base_url = Url::parse(&self.host).map_err(|e| anyhow::anyhow!("Invalid base URL: {}", e))?; base_url .join(path) .map_err(|e| anyhow::anyhow!("Failed to construct URL: {}", e)) } async fn get_oauth_token(&self, config: &OAuthConfig) -> Result { super::oauth::get_oauth_token_async( &config.host, &config.client_id, &config.redirect_url, &config.scopes, ) .await } } impl<'a> ApiRequestBuilder<'a> { pub fn header(mut self, key: &str, value: &str) -> Result { let header_name = HeaderName::from_bytes(key.as_bytes())?; let header_value = HeaderValue::from_str(value)?; self.headers.insert(header_name, header_value); Ok(self) } #[allow(dead_code)] pub fn headers(mut self, headers: HeaderMap) -> Self { self.headers.extend(headers); self } pub async fn api_post(self, payload: &Value) -> Result { let response = self.response_post(payload).await?; ApiResponse::from_response(response).await } pub async fn response_post(self, payload: &Value) -> Result { let request = self.send_request(|url, client| client.post(url)).await?; Ok(request.json(payload).send().await?) } pub async fn api_get(self) -> Result { let response = self.response_get().await?; ApiResponse::from_response(response).await } pub async fn response_get(self) -> Result { let request = self.send_request(|url, client| client.get(url)).await?; Ok(request.send().await?) } async fn send_request(&self, request_builder: F) -> Result where F: FnOnce(url::Url, &Client) -> reqwest::RequestBuilder, { let url = self.client.build_url(self.path)?; let mut request = request_builder(url, &self.client.client); request = request.headers(self.headers.clone()); request = match &self.client.auth { AuthMethod::BearerToken(token) => { request.header("Authorization", format!("Bearer {}", token)) } AuthMethod::ApiKey { header_name, key } => request.header(header_name.as_str(), key), AuthMethod::OAuth(config) => { let token = self.client.get_oauth_token(config).await?; request.header("Authorization", format!("Bearer {}", token)) } AuthMethod::Custom(provider) => { let (header_name, header_value) = provider.get_auth_header().await?; request.header(header_name, header_value) } }; Ok(request) } } impl fmt::Debug for ApiClient { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ApiClient") .field("host", &self.host) .field("auth", &"[auth method]") .field("timeout", &self.timeout) .field("default_headers", &self.default_headers) .finish_non_exhaustive() } }