//! Base HTTP client for makima API. use reqwest::Client; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; /// API client errors. #[derive(Error, Debug)] pub enum ApiError { #[error("HTTP request failed: {0}")] Request(#[from] reqwest::Error), #[error("API error (HTTP {status}): {message}")] Api { status: u16, message: String }, #[error("Failed to parse response: {0}")] Parse(String), } /// HTTP client for makima API. pub struct ApiClient { client: Client, base_url: String, api_key: String, } impl ApiClient { /// Create a new API client. pub fn new(base_url: String, api_key: String) -> Result { let client = Client::builder() .build()?; Ok(Self { client, base_url: base_url.trim_end_matches('/').to_string(), api_key, }) } /// Make a GET request. pub async fn get(&self, path: &str) -> Result { let url = format!("{}{}", self.base_url, path); let response = self.client .get(&url) // Send both headers - server will try tool key first, then API key .header("X-Makima-Tool-Key", &self.api_key) .header("X-Makima-API-Key", &self.api_key) .send() .await?; self.handle_response(response).await } /// Make a POST request with JSON body. pub async fn post( &self, path: &str, body: &B, ) -> Result { let url = format!("{}{}", self.base_url, path); let response = self.client .post(&url) // Send both headers - server will try tool key first, then API key .header("X-Makima-Tool-Key", &self.api_key) .header("X-Makima-API-Key", &self.api_key) .header("Content-Type", "application/json") .json(body) .send() .await?; self.handle_response(response).await } /// Make a POST request without body. pub async fn post_empty(&self, path: &str) -> Result { let url = format!("{}{}", self.base_url, path); let response = self.client .post(&url) // Send both headers - server will try tool key first, then API key .header("X-Makima-Tool-Key", &self.api_key) .header("X-Makima-API-Key", &self.api_key) .send() .await?; self.handle_response(response).await } /// Make a PUT request with JSON body. pub async fn put( &self, path: &str, body: &B, ) -> Result { let url = format!("{}{}", self.base_url, path); let response = self.client .put(&url) // Send both headers - server will try tool key first, then API key .header("X-Makima-Tool-Key", &self.api_key) .header("X-Makima-API-Key", &self.api_key) .header("Content-Type", "application/json") .json(body) .send() .await?; self.handle_response(response).await } /// Make a DELETE request. pub async fn delete(&self, path: &str) -> Result<(), ApiError> { let url = format!("{}{}", self.base_url, path); let response = self.client .delete(&url) .header("X-Makima-Tool-Key", &self.api_key) .header("X-Makima-API-Key", &self.api_key) .send() .await?; let status = response.status(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); return Err(ApiError::Api { status: status.as_u16(), message: body, }); } Ok(()) } /// Handle API response. async fn handle_response( &self, response: reqwest::Response, ) -> Result { let status = response.status(); let status_code = status.as_u16(); if !status.is_success() { let body = response.text().await.unwrap_or_default(); return Err(ApiError::Api { status: status_code, message: body, }); } let body = response.text().await?; // Handle empty responses if body.is_empty() || body == "null" { // Try to parse empty/null as the target type serde_json::from_str::("null") .or_else(|_| serde_json::from_str::("{}")) .map_err(|e| ApiError::Parse(e.to_string())) } else { serde_json::from_str::(&body) .map_err(|e| ApiError::Parse(format!("{}: {}", e, body))) } } }