Skip to main content

openlark_core/api/
responses.rs

1//! API响应类型定义
2//!
3//! 独立的响应处理系统,替代api_resp模块
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// 原始响应数据
9///
10/// `code` 是**双域共槽**:
11/// - 飞书业务信封:装入飞书 `code` 字段(可为 9 位 i32,如 `99991663`);
12/// - HTTP 非 2xx 且无信封:装入合成 HTTP status(如 429/500)。
13///
14/// [`crate::error::ErrorCode::from_code`] 同时含 HTTP status 臂与飞书业务码臂,是双域共槽
15/// 行为正确的依据——**不要**因命名困惑而拆字段;拆共槽属另案(ADR-0004 非目标)。
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct RawResponse {
18    /// 响应代码(双域共槽:飞书业务码或合成 HTTP status;见结构体文档)
19    pub code: i32,
20    /// 响应消息
21    pub msg: String,
22    /// 请求数据ID
23    pub request_id: Option<String>,
24    /// 额外数据
25    pub data: Option<serde_json::Value>,
26    /// 错误信息
27    pub error: Option<ErrorInfo>,
28}
29
30/// 错误信息
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ErrorInfo {
33    /// 错误代码
34    pub code: i32,
35    /// 错误消息
36    pub message: String,
37    /// 错误详情
38    pub details: Option<HashMap<String, serde_json::Value>>,
39}
40
41impl Default for RawResponse {
42    fn default() -> Self {
43        Self {
44            code: 0,
45            msg: "success".to_string(),
46            request_id: None,
47            data: None,
48            error: None,
49        }
50    }
51}
52
53impl RawResponse {
54    /// 创建成功响应
55    pub fn success() -> Self {
56        Self::default()
57    }
58
59    /// 创建带数据的成功响应
60    pub fn success_with_data(data: serde_json::Value) -> Self {
61        Self {
62            data: Some(data),
63            ..Default::default()
64        }
65    }
66
67    /// 创建错误响应
68    pub fn error(code: i32, msg: impl Into<String> + Clone) -> Self {
69        let msg_str = msg.into();
70        Self {
71            code,
72            msg: msg_str.clone(),
73            error: Some(ErrorInfo {
74                code,
75                message: msg_str,
76                details: None,
77            }),
78            ..Default::default()
79        }
80    }
81
82    /// 检查是否成功
83    pub fn is_success(&self) -> bool {
84        self.code == 0
85    }
86
87    /// 获取错误信息
88    pub fn get_error(&self) -> Option<&ErrorInfo> {
89        self.error.as_ref()
90    }
91}
92
93/// 响应格式枚举
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95pub enum ResponseFormat {
96    /// 标准数据格式
97    #[serde(rename = "data")]
98    Data,
99    /// 扁平格式
100    #[serde(rename = "flatten")]
101    Flatten,
102    /// 二进制数据
103    #[serde(rename = "binary")]
104    Binary,
105    /// 文本数据
106    #[serde(rename = "text")]
107    Text,
108    /// 自定义格式
109    #[serde(rename = "custom")]
110    Custom,
111}
112
113impl ResponseFormat {
114    /// 观测/日志用短标签(crate 内部;与解码分派共用,避免双 match)
115    pub(crate) fn as_label(self) -> &'static str {
116        match self {
117            ResponseFormat::Data => "data",
118            ResponseFormat::Flatten => "flatten",
119            ResponseFormat::Binary => "binary",
120            ResponseFormat::Text => "text",
121            ResponseFormat::Custom => "custom",
122        }
123    }
124}
125
126/// API 响应特征:声明解码策略,由 Transport 请求执行层按策略解码。
127///
128/// - [`Self::data_format`] 选择解码路径(不得静默降级到 Data)
129/// - [`Self::requires_payload`]:成功时是否必须解出 `data`(默认 `true`)
130/// - [`Self::empty_success`]:成功且**无** `data` 字段时的显式空载荷(删除类 API);
131///   **禁止**用「能否反序列化 `{}`」探测代替本方法
132/// - Binary / Text / Custom 通过 [`Self::from_binary`] / [`Self::from_text`] /
133///   [`Self::from_custom`] 参与解码,避免运行时 `TypeId` 猜测
134pub trait ApiResponseTrait: Sized + Send + Sync + 'static {
135    /// 获取响应数据格式
136    fn data_format() -> ResponseFormat {
137        ResponseFormat::Data
138    }
139
140    /// 成功响应是否必须携带可解码 payload。
141    /// `()` 等无体响应返回 `false`;默认 `true`。
142    fn requires_payload() -> bool {
143        true
144    }
145
146    /// 成功且响应体无 `data` 字段时的显式空成功值。
147    ///
148    /// 默认 `None`:若同时 [`Self::requires_payload`] 为 true,则解码失败。
149    /// 删除类空 struct 应返回 `Some(Self { .. })`。
150    fn empty_success() -> Option<Self> {
151        None
152    }
153
154    /// Binary 解码:保留文件名 metadata,由类型自行映射。
155    fn from_binary(_file_name: String, _body: Vec<u8>) -> Option<Self> {
156        None
157    }
158
159    /// Text 解码:原始响应体按 UTF-8 文本处理。
160    fn from_text(_text: String) -> Option<Self> {
161        None
162    }
163
164    /// Custom 解码:原始字节 + Content-Type,未实现则解码失败。
165    fn from_custom(_body: Vec<u8>, _content_type: Option<&str>) -> Option<Self> {
166        None
167    }
168}
169
170/// 通用响应结构
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct Response<T> {
173    /// 响应数据
174    pub data: Option<T>,
175    /// 原始响应
176    pub raw_response: RawResponse,
177}
178
179impl<T> Response<T> {
180    /// 创建新响应
181    pub fn new(data: Option<T>, raw_response: RawResponse) -> Self {
182        Self { data, raw_response }
183    }
184
185    /// 创建成功响应
186    pub fn success(data: T) -> Self {
187        Self {
188            data: Some(data),
189            raw_response: RawResponse::success(),
190        }
191    }
192
193    /// 创建空成功响应
194    pub fn success_empty() -> Self {
195        Self {
196            data: None,
197            raw_response: RawResponse::success(),
198        }
199    }
200
201    /// 创建错误响应
202    pub fn error(code: i32, msg: impl Into<String> + Clone) -> Self {
203        Self {
204            data: None,
205            raw_response: RawResponse::error(code, msg),
206        }
207    }
208
209    /// 检查是否成功
210    pub fn is_success(&self) -> bool {
211        self.raw_response.is_success()
212    }
213
214    /// 获取响应代码
215    pub fn code(&self) -> i32 {
216        self.raw_response.code
217    }
218
219    /// 获取响应消息
220    pub fn message(&self) -> &str {
221        &self.raw_response.msg
222    }
223
224    /// 获取响应消息(兼容方法)
225    pub fn msg(&self) -> &str {
226        &self.raw_response.msg
227    }
228
229    /// 获取数据
230    pub fn data(&self) -> Option<&T> {
231        self.data.as_ref()
232    }
233
234    /// 获取原始响应
235    pub fn raw(&self) -> &RawResponse {
236        &self.raw_response
237    }
238
239    /// Canonical finisher:从 `Response<T>` 抽取 typed `T`(#486 起 `extract_response_data`
240    /// 自由函数收敛到此方法)。
241    ///
242    /// `data` 存在则返回;缺失时区分两种失败(#470 user story 12:业务错误不再被误报为空成功):
243    /// - 业务错误(`code != 0`):`api_error` 保留飞书 `code` / `msg`;
244    /// - `code == 0` 缺 `data`:`validation_error`(真正抽取失败)。
245    ///
246    /// 两种失败都经 `map_context` 附 `operation=extract_response_data` +
247    /// `resource=<context>` + 响应 `request_id`。供 `Transport::request_typed`(核心)
248    /// 与持有 `Response<T>` 的 facade 组合层收尾用;leaf 不应直接调用(走 request_typed)。
249    pub fn decode(self, context: &str) -> Result<T, crate::error::CoreError> {
250        if let Some(data) = self.data {
251            return Ok(data);
252        }
253        let raw = self.raw_response;
254        let request_id = raw.request_id.clone();
255        let err = if raw.code != 0 {
256            // 传 raw.code 原值(i32),禁止 as u16 截断;分类经 ErrorCode::from_code
257            crate::error::api_error(raw.code, "response", raw.msg, request_id.clone())
258        } else {
259            crate::error::validation_error("response.data", "服务器没有返回有效的数据")
260        };
261        Err(err.map_context(|ctx| {
262            ctx.set_operation("extract_response_data")
263                .add_context("resource", context);
264            if let Some(req_id) = request_id.as_ref().filter(|r| !r.trim().is_empty()) {
265                ctx.set_request_id(req_id);
266            }
267        }))
268    }
269}
270
271// 为常见类型实现 ApiResponseTrait
272impl ApiResponseTrait for serde_json::Value {}
273// String 默认 Data 格式(JSON envelope);Text 请用自定义类型并覆写 data_format + from_text
274impl ApiResponseTrait for String {}
275impl ApiResponseTrait for Vec<u8> {
276    fn data_format() -> ResponseFormat {
277        ResponseFormat::Binary
278    }
279
280    fn from_binary(_file_name: String, body: Vec<u8>) -> Option<Self> {
281        Some(body)
282    }
283}
284impl ApiResponseTrait for () {
285    fn requires_payload() -> bool {
286        false
287    }
288}
289
290// 类型别名,用于向后兼容
291/// 基础响应类型别名
292pub type BaseResponse<T> = Response<T>;
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_raw_response_default() {
300        let response = RawResponse::default();
301        assert_eq!(response.code, 0);
302        assert_eq!(response.msg, "success");
303        assert!(response.request_id.is_none());
304        assert!(response.data.is_none());
305        assert!(response.error.is_none());
306    }
307
308    #[test]
309    fn test_raw_response_success() {
310        let response = RawResponse::success();
311        assert!(response.is_success());
312    }
313
314    #[test]
315    fn test_raw_response_success_with_data() {
316        let data = serde_json::json!({"key": "value"});
317        let response = RawResponse::success_with_data(data.clone());
318        assert!(response.is_success());
319        assert_eq!(response.data, Some(data));
320    }
321
322    #[test]
323    fn test_raw_response_error() {
324        let response = RawResponse::error(400, "Bad Request");
325        assert!(!response.is_success());
326        assert_eq!(response.code, 400);
327        assert!(response.error.is_some());
328    }
329
330    #[test]
331    fn test_raw_response_get_error() {
332        let response = RawResponse::error(404, "Not Found");
333        let error = response.get_error();
334        assert!(error.is_some());
335        assert_eq!(error.unwrap().code, 404);
336    }
337
338    #[test]
339    fn test_raw_response_serialization() {
340        let response = RawResponse::success_with_data(serde_json::json!({"test": 123}));
341        let json = serde_json::to_string(&response).unwrap();
342        let parsed: RawResponse = serde_json::from_str(&json).expect("JSON 反序列化失败");
343        assert!(parsed.is_success());
344    }
345
346    #[test]
347    fn test_error_info_creation() {
348        let error = ErrorInfo {
349            code: 500,
350            message: "Internal Error".to_string(),
351            details: None,
352        };
353        assert_eq!(error.code, 500);
354        assert_eq!(error.message, "Internal Error");
355    }
356
357    #[test]
358    fn test_response_format() {
359        assert_eq!(ResponseFormat::Data, ResponseFormat::Data);
360        assert_ne!(ResponseFormat::Data, ResponseFormat::Flatten);
361    }
362
363    #[test]
364    fn test_response_format_binary() {
365        assert_eq!(<Vec<u8>>::data_format(), ResponseFormat::Binary);
366    }
367
368    #[test]
369    fn test_response_format_default() {
370        assert_eq!(<()>::data_format(), ResponseFormat::Data);
371    }
372
373    #[test]
374    fn test_response_deserialize_requires_raw_response() {
375        let payload = r#"{"code":400,"msg":"Bad Request"}"#;
376        let parsed = serde_json::from_str::<Response<serde_json::Value>>(payload);
377        assert!(parsed.is_err());
378    }
379
380    #[test]
381    fn test_response_deserialize_with_raw_response_error_keeps_code_and_msg() {
382        let payload = r#"{"raw_response":{"code":400,"msg":"Bad Request","request_id":null,"data":null,"error":null},"data":null}"#;
383        let parsed = serde_json::from_str::<Response<serde_json::Value>>(payload)
384            .expect("JSON 反序列化失败");
385        assert_eq!(parsed.raw_response.code, 400);
386        assert_eq!(parsed.raw_response.msg, "Bad Request");
387        assert!(!parsed.is_success());
388    }
389
390    // Response::decode(#486:extract_response_data 收敛到此方法)
391
392    #[test]
393    fn decode_returns_data_on_success() {
394        let response: Response<String> = Response {
395            data: Some("x".to_string()),
396            raw_response: RawResponse::success(),
397        };
398        assert_eq!(response.decode("测试").unwrap(), "x");
399    }
400
401    #[test]
402    fn decode_missing_data_is_validation_error_with_context() {
403        let response: Response<String> = Response {
404            data: None,
405            raw_response: RawResponse::success(),
406        };
407        let err = response.decode("测试").expect_err("缺 data 应报错");
408        let ctx = err.ctx();
409        assert_eq!(ctx.operation(), Some("extract_response_data"));
410        assert_eq!(ctx.get_context("resource"), Some("测试"));
411    }
412
413    /// 业务错误:保留 msg、request_id;#544 不截断——`raw_code` 原样 + `from_code` 分类 + Display 真码。
414    #[test]
415    fn decode_business_error_preserves_msg_and_classifies_raw_code() {
416        let response: Response<String> = Response {
417            data: None,
418            raw_response: RawResponse {
419                code: 99991663,
420                msg: "tenant access token invalid".to_string(),
421                request_id: Some("rid-x".to_string()),
422                ..RawResponse::success()
423            },
424        };
425        let err = response.decode("测试").expect_err("业务错误应报错");
426        assert_eq!(err.ctx().request_id(), Some("rid-x"));
427        match err {
428            crate::error::CoreError::Api(api) => {
429                assert!(
430                    api.message.contains("tenant access token invalid"),
431                    "msg preserved: {}",
432                    api.message
433                );
434                assert_eq!(
435                    api.raw_code, 99991663,
436                    "raw_code must preserve full i32 feishu code"
437                );
438                assert_eq!(
439                    api.code,
440                    crate::error::ErrorCode::TenantAccessTokenInvalid,
441                    "classification must use from_code without u16 truncation"
442                );
443                let display = api.to_string();
444                assert!(
445                    display.contains("99991663"),
446                    "Display must show real code, not truncated garbage: {display}"
447                );
448            }
449            other => panic!("expected Api for business error, got: {other:?}"),
450        }
451    }
452}