gewe_http/
client.rs

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        // 先解析为通用 JSON,检查 ret 状态
66        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        // 如果 ret != 200,直接返回 API 错误,不尝试解析 data
77        if ret != 200 {
78            return Err(GeweError::Api {
79                code: ret,
80                message: msg,
81            });
82        }
83
84        // ret == 200 时,再解析完整的响应结构
85        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        // 测试基本路径
115        assert_eq!(
116            client.endpoint("api/test"),
117            "https://api.example.com/api/test"
118        );
119
120        // 测试带前导斜杠的路径
121        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        // 测试包含非法字符的 token
130        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        // base_url 有尾部斜杠,path 有前导斜杠
152        assert_eq!(
153            client.endpoint("/api/test"),
154            "https://api.example.com/api/test"
155        );
156
157        // base_url 有尾部斜杠,path 无前导斜杠
158        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        // 虽然通常不用 query params,但测试边界情况
170        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        // 测试 ApiEnvelope 的解析
179        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        // 测试没有 data 字段的响应
197        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}