use indexmap::IndexMap;
use wecomx_transport::{
HttpEndpoint, ResponseEnvelope,
backend::protocol::{ApiResponse, validate_api_response},
};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RequireAuth;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct SuppressAuth;
pub const DEFAULT_AUTH_ENDPOINT: &str =
"https://qyapi.weixin.qq.com/cgi-bin/aibot/cli/get_cli_config";
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct FlatApiResponse {
pub errcode: Option<i64>,
pub errmsg: Option<String>,
pub results_json: Option<String>,
#[serde(flatten)]
pub extra: IndexMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NestedRes;
impl ResponseEnvelope for NestedRes {
fn decode(
&self,
url: &str,
body: serde_json::Value,
) -> std::result::Result<ApiResponse, wecomx_transport::Error> {
let flat: FlatApiResponse =
serde_json::from_value(body).map_err(|e| wecomx_transport::Error::Parse {
message: format!("Parse FlatApiResponse failed for {url}: {e:#}"),
endpoint: url.to_string(),
body: Box::new(serde_json::Value::Null),
source: Some(e),
})?;
let flat = validate_flat_api_response(url, flat)?;
let results_json =
flat.results_json
.as_deref()
.ok_or_else(|| wecomx_transport::Error::Parse {
message: "API response missing `results_json` field".to_string(),
endpoint: url.to_string(),
body: Box::new(serde_json::to_value(&flat).unwrap_or_default()),
source: None,
})?;
let inner: ApiResponse =
serde_json::from_str(results_json).map_err(|e| wecomx_transport::Error::Parse {
message: format!("Parse `results_json` JSON failed: {e:#}"),
endpoint: url.to_string(),
body: Box::new(serde_json::Value::String(results_json.to_string())),
source: Some(e),
})?;
validate_api_response(url, inner)
}
fn name(&self) -> &'static str {
"nested"
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FlatRes;
impl ResponseEnvelope for FlatRes {
fn decode(
&self,
url: &str,
body: serde_json::Value,
) -> std::result::Result<ApiResponse, wecomx_transport::Error> {
let flat: FlatApiResponse =
serde_json::from_value(body).map_err(|e| wecomx_transport::Error::Parse {
message: format!("Parse FlatApiResponse failed for {url}: {e:#}"),
endpoint: url.to_string(),
body: Box::new(serde_json::Value::Null),
source: Some(e),
})?;
let flat = validate_flat_api_response(url, flat)?;
Ok(ApiResponse {
result: Some(serde_json::to_string(&flat.extra).unwrap_or_default()),
error: None,
taskid: None,
poll_mode: None,
long_task_poll: None,
extra: Default::default(),
})
}
fn name(&self) -> &'static str {
"flat"
}
}
fn validate_flat_api_response(
url: &str,
data: FlatApiResponse,
) -> std::result::Result<FlatApiResponse, wecomx_transport::Error> {
let code = data.errcode.unwrap_or(0);
if code != 0 {
return Err(wecomx_transport::Error::Api {
message: data
.errmsg
.clone()
.unwrap_or_else(|| "Unknown error".to_string()),
action: url.to_string(),
code: Some(code),
body: Box::new(serde_json::to_value(&data).unwrap_or_default()),
})
.inspect_err(|e| tracing::error!(error = %e, "API error response"));
}
Ok(data)
}
pub fn auth_endpoint(url: &str) -> wecomx_transport::Endpoint {
wecomx_transport::Endpoint::new()
.with(HttpEndpoint::from_url(url).with_res_envelope(FlatRes))
.with(SuppressAuth)
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn flat_res_returns_extra_on_success() {
let body = json!({"errcode": 0, "errmsg": "ok", "token": "t1"});
let resp = FlatRes.decode("/auth", body).unwrap();
let result: serde_json::Value =
serde_json::from_str(resp.result.as_deref().unwrap()).unwrap();
assert_eq!(result, json!({"token": "t1"}));
}
#[test]
fn flat_res_missing_errcode_is_ok() {
let resp = FlatRes.decode("/auth", json!({"token": "t1"})).unwrap();
let result: serde_json::Value =
serde_json::from_str(resp.result.as_deref().unwrap()).unwrap();
assert_eq!(result, json!({"token": "t1"}));
}
#[test]
fn flat_res_errcode_is_api_error() {
let err = FlatRes
.decode(
"/auth",
json!({"errcode": 853000, "errmsg": "invalid credential"}),
)
.unwrap_err();
match err {
wecomx_transport::Error::Api {
message,
action,
code,
body,
} => {
assert_eq!(message, "invalid credential");
assert_eq!(action, "/auth");
assert_eq!(code, Some(853000));
assert_eq!(body["errcode"], json!(853000));
assert_eq!(body["errmsg"], json!("invalid credential"));
}
other => panic!("expected Api error, got {other:?}"),
}
}
#[test]
fn flat_res_missing_errmsg_falls_back_to_default() {
let err = FlatRes
.decode("/auth", json!({"errcode": 853004}))
.unwrap_err();
match err {
wecomx_transport::Error::Api { message, code, .. } => {
assert_eq!(message, "Unknown error");
assert_eq!(code, Some(853004));
}
other => panic!("expected Api error, got {other:?}"),
}
}
}