1use indexmap::IndexMap;
18use wecomx_transport::{
19 HttpEndpoint, ResponseEnvelope,
20 backend::protocol::{ApiResponse, validate_api_response},
21};
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub struct RequireAuth;
30
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct SuppressAuth;
37
38pub const DEFAULT_AUTH_ENDPOINT: &str =
40 "https://qyapi.weixin.qq.com/cgi-bin/aibot/cli/get_cli_config";
41
42#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
46pub struct FlatApiResponse {
47 pub errcode: Option<i64>,
48 pub errmsg: Option<String>,
49 pub results_json: Option<String>,
50 #[serde(flatten)]
51 pub extra: IndexMap<String, serde_json::Value>,
52}
53
54#[derive(Debug, Clone, Copy, Default)]
60pub struct NestedRes;
61
62impl ResponseEnvelope for NestedRes {
63 fn decode(
64 &self,
65 url: &str,
66 body: serde_json::Value,
67 ) -> std::result::Result<ApiResponse, wecomx_transport::Error> {
68 let flat: FlatApiResponse =
70 serde_json::from_value(body).map_err(|e| wecomx_transport::Error::Parse {
71 message: format!("Parse FlatApiResponse failed for {url}: {e:#}"),
72 endpoint: url.to_string(),
73 body: Box::new(serde_json::Value::Null),
74 source: Some(e),
75 })?;
76 let flat = validate_flat_api_response(url, flat)?;
77
78 let results_json =
81 flat.results_json
82 .as_deref()
83 .ok_or_else(|| wecomx_transport::Error::Parse {
84 message: "API response missing `results_json` field".to_string(),
85 endpoint: url.to_string(),
86 body: Box::new(serde_json::to_value(&flat).unwrap_or_default()),
87 source: None,
88 })?;
89
90 let inner: ApiResponse =
91 serde_json::from_str(results_json).map_err(|e| wecomx_transport::Error::Parse {
92 message: format!("Parse `results_json` JSON failed: {e:#}"),
93 endpoint: url.to_string(),
94 body: Box::new(serde_json::Value::String(results_json.to_string())),
95 source: Some(e),
96 })?;
97
98 validate_api_response(url, inner)
99 }
100
101 fn name(&self) -> &'static str {
102 "nested"
103 }
104}
105
106#[derive(Debug, Clone, Copy, Default)]
114pub struct FlatRes;
115
116impl ResponseEnvelope for FlatRes {
117 fn decode(
118 &self,
119 url: &str,
120 body: serde_json::Value,
121 ) -> std::result::Result<ApiResponse, wecomx_transport::Error> {
122 let flat: FlatApiResponse =
123 serde_json::from_value(body).map_err(|e| wecomx_transport::Error::Parse {
124 message: format!("Parse FlatApiResponse failed for {url}: {e:#}"),
125 endpoint: url.to_string(),
126 body: Box::new(serde_json::Value::Null),
127 source: Some(e),
128 })?;
129
130 let flat = validate_flat_api_response(url, flat)?;
131
132 Ok(ApiResponse {
134 result: Some(serde_json::to_string(&flat.extra).unwrap_or_default()),
135 error: None,
136 taskid: None,
137 poll_mode: None,
138 long_task_poll: None,
139 extra: Default::default(),
140 })
141 }
142
143 fn name(&self) -> &'static str {
144 "flat"
145 }
146}
147
148fn validate_flat_api_response(
152 url: &str,
153 data: FlatApiResponse,
154) -> std::result::Result<FlatApiResponse, wecomx_transport::Error> {
155 let code = data.errcode.unwrap_or(0);
156 if code != 0 {
157 return Err(wecomx_transport::Error::Api {
158 message: data
159 .errmsg
160 .clone()
161 .unwrap_or_else(|| "Unknown error".to_string()),
162 action: url.to_string(),
163 code: Some(code),
164 body: Box::new(serde_json::to_value(&data).unwrap_or_default()),
165 })
166 .inspect_err(|e| tracing::error!(error = %e, "API error response"));
167 }
168 Ok(data)
169}
170
171pub fn auth_endpoint(url: &str) -> wecomx_transport::Endpoint {
179 wecomx_transport::Endpoint::new()
180 .with(HttpEndpoint::from_url(url).with_res_envelope(FlatRes))
181 .with(SuppressAuth)
182}
183
184#[cfg(test)]
185mod tests {
186 use serde_json::json;
197
198 use super::*;
199
200 #[test]
204 fn flat_res_returns_extra_on_success() {
205 let body = json!({"errcode": 0, "errmsg": "ok", "token": "t1"});
206 let resp = FlatRes.decode("/auth", body).unwrap();
207 let result: serde_json::Value =
208 serde_json::from_str(resp.result.as_deref().unwrap()).unwrap();
209 assert_eq!(result, json!({"token": "t1"}));
210 }
211
212 #[test]
216 fn flat_res_missing_errcode_is_ok() {
217 let resp = FlatRes.decode("/auth", json!({"token": "t1"})).unwrap();
218 let result: serde_json::Value =
219 serde_json::from_str(resp.result.as_deref().unwrap()).unwrap();
220 assert_eq!(result, json!({"token": "t1"}));
221 }
222
223 #[test]
227 fn flat_res_errcode_is_api_error() {
228 let err = FlatRes
229 .decode(
230 "/auth",
231 json!({"errcode": 853000, "errmsg": "invalid credential"}),
232 )
233 .unwrap_err();
234 match err {
235 wecomx_transport::Error::Api {
236 message,
237 action,
238 code,
239 body,
240 } => {
241 assert_eq!(message, "invalid credential");
242 assert_eq!(action, "/auth");
243 assert_eq!(code, Some(853000));
244 assert_eq!(body["errcode"], json!(853000));
245 assert_eq!(body["errmsg"], json!("invalid credential"));
246 }
247 other => panic!("expected Api error, got {other:?}"),
248 }
249 }
250
251 #[test]
255 fn flat_res_missing_errmsg_falls_back_to_default() {
256 let err = FlatRes
257 .decode("/auth", json!({"errcode": 853004}))
258 .unwrap_err();
259 match err {
260 wecomx_transport::Error::Api { message, code, .. } => {
261 assert_eq!(message, "Unknown error");
262 assert_eq!(code, Some(853004));
263 }
264 other => panic!("expected Api error, got {other:?}"),
265 }
266 }
267}