Skip to main content

edgebase_core/
http_client.rs

1//! EdgeBase Rust SDK — Internal HTTP client (#133: set_context / X-EdgeBase-Context removed)
2
3use crate::Error;
4use reqwest::{Client, Method};
5use serde_json::Value;
6use std::time::Duration;
7
8pub struct HttpClient {
9    client: Client,
10    base_url: String,
11    service_key: String,
12    #[cfg_attr(not(test), allow(dead_code))]
13    timeout_ms: Option<u64>,
14}
15
16fn parse_timeout_ms(raw: Option<&str>) -> Option<u64> {
17    raw.and_then(|value| value.trim().parse::<u64>().ok())
18        .filter(|value| *value > 0)
19}
20
21impl HttpClient {
22    pub fn new(base_url: &str, service_key: &str) -> Result<Self, Error> {
23        let url = base_url.trim_end_matches('/').to_string();
24        let timeout_ms = parse_timeout_ms(std::env::var("EDGEBASE_HTTP_TIMEOUT_MS").ok().as_deref());
25        let mut builder = Client::builder();
26        if let Some(timeout_ms) = timeout_ms {
27            builder = builder.timeout(Duration::from_millis(timeout_ms));
28        }
29        Ok(Self {
30            client: builder.build()?,
31            base_url: url,
32            service_key: service_key.to_string(),
33            timeout_ms,
34        })
35    }
36
37    pub fn base_url(&self) -> &str {
38        &self.base_url
39    }
40
41    #[cfg_attr(not(test), allow(dead_code))]
42    pub(crate) fn timeout_ms(&self) -> Option<u64> {
43        self.timeout_ms
44    }
45
46    #[cfg_attr(not(test), allow(dead_code))]
47    pub(crate) fn parse_timeout_ms_for_tests(raw: Option<&str>) -> Option<u64> {
48        parse_timeout_ms(raw)
49    }
50
51    fn build_request(&self, method: Method, path: &str) -> reqwest::RequestBuilder {
52        let url = format!("{}{}", self.base_url, path);
53        let mut req = self.client.request(method, &url);
54        if !self.service_key.is_empty() {
55            req = req.header("X-EdgeBase-Service-Key", &self.service_key);
56            req = req.header("Authorization", format!("Bearer {}", self.service_key));
57        }
58        req
59    }
60
61    async fn send(&self, req: reqwest::RequestBuilder) -> Result<Value, Error> {
62        let resp = req.send().await?;
63        let status = resp.status();
64        let text = resp.text().await?;
65        if !status.is_success() {
66            let msg = serde_json::from_str::<Value>(&text)
67                .ok()
68                .and_then(|v| {
69                    v.get("error")
70                        .or_else(|| v.get("message"))
71                        .and_then(|m| m.as_str())
72                        .map(|s| s.to_string())
73                })
74                .unwrap_or_else(|| text.clone());
75            return Err(Error::Api {
76                status: status.as_u16(),
77                message: msg,
78            });
79        }
80        if text.is_empty() {
81            return Ok(Value::Null);
82        }
83        Ok(serde_json::from_str(&text)?)
84    }
85
86    pub async fn get(&self, path: &str) -> Result<Value, Error> {
87        let req = self.build_request(Method::GET, path);
88        self.send(req).await
89    }
90
91    pub async fn get_with_query(&self, path: &str, query: &std::collections::HashMap<String, String>) -> Result<Value, Error> {
92        let req = self.build_request(Method::GET, path).query(query);
93        self.send(req).await
94    }
95
96    pub async fn post(&self, path: &str, body: &Value) -> Result<Value, Error> {
97        let req = self.build_request(Method::POST, path).json(body);
98        self.send(req).await
99    }
100
101    pub async fn post_with_query(&self, path: &str, body: &Value, query: &std::collections::HashMap<String, String>) -> Result<Value, Error> {
102        let req = self.build_request(Method::POST, path).json(body).query(query);
103        self.send(req).await
104    }
105
106    pub async fn patch(&self, path: &str, body: &Value) -> Result<Value, Error> {
107        let req = self.build_request(Method::PATCH, path).json(body);
108        self.send(req).await
109    }
110
111    pub async fn delete(&self, path: &str) -> Result<Value, Error> {
112        let req = self.build_request(Method::DELETE, path);
113        self.send(req).await
114    }
115
116    pub async fn delete_with_body(&self, path: &str, body: &Value) -> Result<Value, Error> {
117        let req = self.build_request(Method::DELETE, path).json(body);
118        self.send(req).await
119    }
120
121    /// HEAD request — returns `true` if the resource exists (2xx status).
122    pub async fn head(&self, path: &str) -> Result<bool, Error> {
123        let req = self.build_request(Method::HEAD, path);
124        let resp = req.send().await?;
125        Ok(resp.status().is_success())
126    }
127
128    pub async fn put(&self, path: &str, body: &Value) -> Result<Value, Error> {
129        let req = self.build_request(Method::PUT, path).json(body);
130        self.send(req).await
131    }
132
133    pub async fn put_with_query(&self, path: &str, body: &Value, query: &std::collections::HashMap<String, String>) -> Result<Value, Error> {
134        let req = self.build_request(Method::PUT, path).json(body).query(query);
135        self.send(req).await
136    }
137
138    /// Multipart file upload.
139    pub async fn upload_multipart(
140        &self, path: &str, key: &str, data: Vec<u8>, content_type: &str,
141    ) -> Result<Value, Error> {
142        use reqwest::multipart::{Form, Part};
143        let part = Part::bytes(data)
144            .file_name(key.to_string())
145            .mime_str(content_type)
146            .map_err(|e| Error::Url(e.to_string()))?;
147        let form = Form::new()
148            .part("file", part)
149            .text("key", key.to_string());
150        let url = format!("{}{}", self.base_url, path);
151        let mut req = self.client.post(&url).multipart(form);
152        if !self.service_key.is_empty() {
153            req = req.header("X-EdgeBase-Service-Key", &self.service_key);
154            req = req.header("Authorization", format!("Bearer {}", self.service_key));
155        }
156        self.send(req).await
157    }
158
159    /// POST raw bytes (for multipart upload-part).
160    pub async fn post_bytes(&self, path: &str, data: Vec<u8>, content_type: &str) -> Result<Value, Error> {
161        let req = self.build_request(Method::POST, path)
162            .header("Content-Type", content_type)
163            .body(data);
164        self.send(req).await
165    }
166
167    /// Download raw bytes.
168    pub async fn download_raw(&self, path: &str) -> Result<Vec<u8>, Error> {
169        let req = self.build_request(Method::GET, path);
170        let resp = req.send().await?;
171        let status = resp.status();
172        if !status.is_success() {
173            let msg = resp.text().await.unwrap_or_default();
174            return Err(Error::Api { status: status.as_u16(), message: msg });
175        }
176        Ok(resp.bytes().await.map(|b| b.to_vec())?)
177    }
178}