summaryrefslogtreecommitdiff
path: root/makima/src/daemon/api/client.rs
blob: 2318d5a08a3901436af8fa14819b142081264047 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! 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)
            // 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<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)
            // 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<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> {
        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<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)
            // 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
    }

    /// 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)))
        }
    }
}