1use gewe_core::{ApiEnvelope, GeweError};
2use reqwest::{Client, ClientBuilder};
3use serde::de::DeserializeOwned;
4use serde::Serialize;
5use serde_json::Value;
6use std::time::Duration;
7
8#[derive(Clone)]
9pub struct GeweHttpClient {
10 client: Client,
11 #[cfg_attr(test, allow(dead_code))]
12 pub(crate) base_url: String,
13}
14
15impl GeweHttpClient {
16 pub fn new(token: impl Into<String>, base_url: impl Into<String>) -> Result<Self, GeweError> {
17 let mut headers = reqwest::header::HeaderMap::new();
18 let token = token.into();
19 headers.insert(
20 "X-GEWE-TOKEN",
21 reqwest::header::HeaderValue::from_str(&token)
22 .map_err(|e| GeweError::Http(e.to_string()))?,
23 );
24 let client = ClientBuilder::new()
25 .default_headers(headers)
26 .pool_idle_timeout(Duration::from_secs(90))
27 .timeout(Duration::from_secs(15))
28 .build()
29 .map_err(|e| GeweError::Http(e.to_string()))?;
30 Ok(Self {
31 client,
32 base_url: base_url.into(),
33 })
34 }
35
36 pub(crate) fn endpoint(&self, path: &str) -> String {
37 format!(
38 "{}/{}",
39 self.base_url.trim_end_matches('/'),
40 path.trim_start_matches('/')
41 )
42 }
43
44 pub(crate) async fn post_api<B, R>(
45 &self,
46 path: &str,
47 body: &B,
48 ) -> Result<ApiEnvelope<R>, GeweError>
49 where
50 B: Serialize + ?Sized,
51 R: DeserializeOwned,
52 {
53 let resp = self
54 .client
55 .post(self.endpoint(path))
56 .json(body)
57 .send()
58 .await
59 .map_err(|e| GeweError::Http(e.to_string()))?;
60 let text = resp
61 .text()
62 .await
63 .map_err(|e| GeweError::Decode(format!("read body failed: {e}")))?;
64
65 let raw: Value = serde_json::from_str(&text)
67 .map_err(|e| GeweError::Decode(format!("{e}; body={text}")))?;
68
69 let ret = raw.get("ret").and_then(|v| v.as_i64()).unwrap_or(-1) as i32;
70 let msg = raw
71 .get("msg")
72 .and_then(|v| v.as_str())
73 .unwrap_or("unknown")
74 .to_string();
75
76 if ret != 200 {
78 return Err(GeweError::Api {
79 code: ret,
80 message: msg,
81 });
82 }
83
84 let env: ApiEnvelope<R> = serde_json::from_str(&text)
86 .map_err(|e| GeweError::Decode(format!("{e}; body={text}")))?;
87
88 Ok(env)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use gewe_core::ApiEnvelope;
96 use serde::{Deserialize, Serialize};
97
98 #[derive(Debug, Serialize, Deserialize, PartialEq)]
99 struct TestData {
100 value: String,
101 }
102
103 #[test]
104 fn test_client_creation() {
105 let client = GeweHttpClient::new("test_token", "https://api.example.com");
106 assert!(client.is_ok());
107 }
108
109 #[test]
110 fn test_client_endpoint_construction() {
111 let client = GeweHttpClient::new("token", "https://api.example.com")
112 .expect("Failed to create client");
113
114 assert_eq!(
116 client.endpoint("api/test"),
117 "https://api.example.com/api/test"
118 );
119
120 assert_eq!(
122 client.endpoint("/api/test"),
123 "https://api.example.com/api/test"
124 );
125 }
126
127 #[test]
128 fn test_client_with_invalid_token() {
129 let result = GeweHttpClient::new("invalid\ntoken", "https://api.example.com");
131 assert!(result.is_err());
132 if let Err(e) = result {
133 assert!(matches!(e, GeweError::Http(_)));
134 }
135 }
136
137 #[test]
138 fn test_client_clone() {
139 let client = GeweHttpClient::new("token", "https://api.example.com")
140 .expect("Failed to create client");
141 let cloned = client.clone();
142
143 assert_eq!(client.base_url, cloned.base_url);
144 }
145
146 #[test]
147 fn test_endpoint_with_trailing_slashes() {
148 let client = GeweHttpClient::new("token", "https://api.example.com/")
149 .expect("Failed to create client");
150
151 assert_eq!(
153 client.endpoint("/api/test"),
154 "https://api.example.com/api/test"
155 );
156
157 assert_eq!(
159 client.endpoint("api/test"),
160 "https://api.example.com/api/test"
161 );
162 }
163
164 #[test]
165 fn test_endpoint_with_query_params() {
166 let client = GeweHttpClient::new("token", "https://api.example.com")
167 .expect("Failed to create client");
168
169 assert_eq!(
171 client.endpoint("api/test?key=value"),
172 "https://api.example.com/api/test?key=value"
173 );
174 }
175
176 #[test]
177 fn test_api_envelope_parsing() {
178 let json = r#"{
180 "ret": 200,
181 "msg": "success",
182 "data": {
183 "value": "test"
184 }
185 }"#;
186
187 let envelope: ApiEnvelope<TestData> = serde_json::from_str(json).unwrap();
188 assert_eq!(envelope.ret, 200);
189 assert_eq!(envelope.msg, "success");
190 assert!(envelope.data.is_some());
191 assert_eq!(envelope.data.unwrap().value, "test");
192 }
193
194 #[test]
195 fn test_api_envelope_without_data() {
196 let json = r#"{
198 "ret": 200,
199 "msg": "success"
200 }"#;
201
202 let envelope: ApiEnvelope<TestData> = serde_json::from_str(json).unwrap();
203 assert_eq!(envelope.ret, 200);
204 assert_eq!(envelope.msg, "success");
205 assert!(envelope.data.is_none());
206 }
207
208 #[test]
209 fn test_empty_base_url() {
210 let client = GeweHttpClient::new("token", "").expect("Failed to create client");
211
212 assert_eq!(client.endpoint("api/test"), "/api/test");
213 }
214}