summaryrefslogtreecommitdiff
path: root/makima/src/daemon/api/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/api/client.rs')
-rw-r--r--makima/src/daemon/api/client.rs129
1 files changed, 129 insertions, 0 deletions
diff --git a/makima/src/daemon/api/client.rs b/makima/src/daemon/api/client.rs
new file mode 100644
index 0000000..b27d606
--- /dev/null
+++ b/makima/src/daemon/api/client.rs
@@ -0,0 +1,129 @@
+//! 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<Self, ApiError> {
+ 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<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> {
+ let url = format!("{}{}", self.base_url, path);
+ let response = self.client
+ .get(&url)
+ .header("X-Makima-Tool-Key", &self.api_key)
+ .send()
+ .await?;
+
+ self.handle_response(response).await
+ }
+
+ /// Make a POST request with JSON body.
+ pub async fn post<T: DeserializeOwned, B: Serialize>(
+ &self,
+ path: &str,
+ body: &B,
+ ) -> Result<T, ApiError> {
+ let url = format!("{}{}", self.base_url, path);
+ let response = self.client
+ .post(&url)
+ .header("X-Makima-Tool-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<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> {
+ let url = format!("{}{}", self.base_url, path);
+ let response = self.client
+ .post(&url)
+ .header("X-Makima-Tool-Key", &self.api_key)
+ .send()
+ .await?;
+
+ self.handle_response(response).await
+ }
+
+ /// Make a PUT request with JSON body.
+ pub async fn put<T: DeserializeOwned, B: Serialize>(
+ &self,
+ path: &str,
+ body: &B,
+ ) -> Result<T, ApiError> {
+ let url = format!("{}{}", self.base_url, path);
+ let response = self.client
+ .put(&url)
+ .header("X-Makima-Tool-Key", &self.api_key)
+ .header("Content-Type", "application/json")
+ .json(body)
+ .send()
+ .await?;
+
+ self.handle_response(response).await
+ }
+
+ /// Handle API response.
+ async fn handle_response<T: DeserializeOwned>(
+ &self,
+ response: reqwest::Response,
+ ) -> Result<T, ApiError> {
+ 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::<T>("null")
+ .or_else(|_| serde_json::from_str::<T>("{}"))
+ .map_err(|e| ApiError::Parse(e.to_string()))
+ } else {
+ serde_json::from_str::<T>(&body)
+ .map_err(|e| ApiError::Parse(format!("{}: {}", e, body)))
+ }
+ }
+}