openlark-core 0.19.0

OpenLark 核心基础设施 crate - HTTP 客户端、错误处理、认证和核心工具
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! API响应类型定义
//!
//! 独立的响应处理系统,替代api_resp模块

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// 原始响应数据
///
/// `code` 是**双域共槽**:
/// - 飞书业务信封:装入飞书 `code` 字段(可为 9 位 i32,如 `99991663`);
/// - HTTP 非 2xx 且无信封:装入合成 HTTP status(如 429/500)。
///
/// [`crate::error::ErrorCode::from_code`] 同时含 HTTP status 臂与飞书业务码臂,是双域共槽
/// 行为正确的依据——**不要**因命名困惑而拆字段;拆共槽属另案(ADR-0004 非目标)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawResponse {
    /// 响应代码(双域共槽:飞书业务码或合成 HTTP status;见结构体文档)
    pub code: i32,
    /// 响应消息
    pub msg: String,
    /// 请求数据ID
    pub request_id: Option<String>,
    /// 额外数据
    pub data: Option<serde_json::Value>,
    /// 错误信息
    pub error: Option<ErrorInfo>,
}

/// 错误信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
    /// 错误代码
    pub code: i32,
    /// 错误消息
    pub message: String,
    /// 错误详情
    pub details: Option<HashMap<String, serde_json::Value>>,
}

impl Default for RawResponse {
    fn default() -> Self {
        Self {
            code: 0,
            msg: "success".to_string(),
            request_id: None,
            data: None,
            error: None,
        }
    }
}

impl RawResponse {
    /// 创建成功响应
    pub fn success() -> Self {
        Self::default()
    }

    /// 创建带数据的成功响应
    pub fn success_with_data(data: serde_json::Value) -> Self {
        Self {
            data: Some(data),
            ..Default::default()
        }
    }

    /// 创建错误响应
    pub fn error(code: i32, msg: impl Into<String> + Clone) -> Self {
        let msg_str = msg.into();
        Self {
            code,
            msg: msg_str.clone(),
            error: Some(ErrorInfo {
                code,
                message: msg_str,
                details: None,
            }),
            ..Default::default()
        }
    }

    /// 检查是否成功
    pub fn is_success(&self) -> bool {
        self.code == 0
    }

    /// 获取错误信息
    pub fn get_error(&self) -> Option<&ErrorInfo> {
        self.error.as_ref()
    }
}

/// 响应格式枚举
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResponseFormat {
    /// 标准数据格式
    #[serde(rename = "data")]
    Data,
    /// 扁平格式
    #[serde(rename = "flatten")]
    Flatten,
    /// 二进制数据
    #[serde(rename = "binary")]
    Binary,
    /// 文本数据
    #[serde(rename = "text")]
    Text,
    /// 自定义格式
    #[serde(rename = "custom")]
    Custom,
}

impl ResponseFormat {
    /// 观测/日志用短标签(crate 内部;与解码分派共用,避免双 match)
    pub(crate) fn as_label(self) -> &'static str {
        match self {
            ResponseFormat::Data => "data",
            ResponseFormat::Flatten => "flatten",
            ResponseFormat::Binary => "binary",
            ResponseFormat::Text => "text",
            ResponseFormat::Custom => "custom",
        }
    }
}

/// API 响应特征:声明解码策略,由 Transport 请求执行层按策略解码。
///
/// - [`Self::data_format`] 选择解码路径(不得静默降级到 Data)
/// - [`Self::requires_payload`]:成功时是否必须解出 `data`(默认 `true`)
/// - [`Self::empty_success`]:成功且**无** `data` 字段时的显式空载荷(删除类 API);
///   **禁止**用「能否反序列化 `{}`」探测代替本方法
/// - Binary / Text / Custom 通过 [`Self::from_binary`] / [`Self::from_text`] /
///   [`Self::from_custom`] 参与解码,避免运行时 `TypeId` 猜测
pub trait ApiResponseTrait: Sized + Send + Sync + 'static {
    /// 获取响应数据格式
    fn data_format() -> ResponseFormat {
        ResponseFormat::Data
    }

    /// 成功响应是否必须携带可解码 payload。
    /// `()` 等无体响应返回 `false`;默认 `true`。
    fn requires_payload() -> bool {
        true
    }

    /// 成功且响应体无 `data` 字段时的显式空成功值。
    ///
    /// 默认 `None`:若同时 [`Self::requires_payload`] 为 true,则解码失败。
    /// 删除类空 struct 应返回 `Some(Self { .. })`。
    fn empty_success() -> Option<Self> {
        None
    }

    /// Binary 解码:保留文件名 metadata,由类型自行映射。
    fn from_binary(_file_name: String, _body: Vec<u8>) -> Option<Self> {
        None
    }

    /// Text 解码:原始响应体按 UTF-8 文本处理。
    fn from_text(_text: String) -> Option<Self> {
        None
    }

    /// Custom 解码:原始字节 + Content-Type,未实现则解码失败。
    fn from_custom(_body: Vec<u8>, _content_type: Option<&str>) -> Option<Self> {
        None
    }
}

/// 通用响应结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response<T> {
    /// 响应数据
    pub data: Option<T>,
    /// 原始响应
    pub raw_response: RawResponse,
}

impl<T> Response<T> {
    /// 创建新响应
    pub fn new(data: Option<T>, raw_response: RawResponse) -> Self {
        Self { data, raw_response }
    }

    /// 创建成功响应
    pub fn success(data: T) -> Self {
        Self {
            data: Some(data),
            raw_response: RawResponse::success(),
        }
    }

    /// 创建空成功响应
    pub fn success_empty() -> Self {
        Self {
            data: None,
            raw_response: RawResponse::success(),
        }
    }

    /// 创建错误响应
    pub fn error(code: i32, msg: impl Into<String> + Clone) -> Self {
        Self {
            data: None,
            raw_response: RawResponse::error(code, msg),
        }
    }

    /// 检查是否成功
    pub fn is_success(&self) -> bool {
        self.raw_response.is_success()
    }

    /// 获取响应代码
    pub fn code(&self) -> i32 {
        self.raw_response.code
    }

    /// 获取响应消息
    pub fn message(&self) -> &str {
        &self.raw_response.msg
    }

    /// 获取响应消息(兼容方法)
    pub fn msg(&self) -> &str {
        &self.raw_response.msg
    }

    /// 获取数据
    pub fn data(&self) -> Option<&T> {
        self.data.as_ref()
    }

    /// 获取原始响应
    pub fn raw(&self) -> &RawResponse {
        &self.raw_response
    }

    /// Canonical finisher:从 `Response<T>` 抽取 typed `T`(#486 起 `extract_response_data`
    /// 自由函数收敛到此方法)。
    ///
    /// `data` 存在则返回;缺失时区分两种失败(#470 user story 12:业务错误不再被误报为空成功):
    /// - 业务错误(`code != 0`):`api_error` 保留飞书 `code` / `msg`;
    /// - `code == 0` 缺 `data`:`validation_error`(真正抽取失败)。
    ///
    /// 两种失败都经 `map_context` 附 `operation=extract_response_data` +
    /// `resource=<context>` + 响应 `request_id`。供 `Transport::request_typed`(核心)
    /// 与持有 `Response<T>` 的 facade 组合层收尾用;leaf 不应直接调用(走 request_typed)。
    pub fn decode(self, context: &str) -> Result<T, crate::error::CoreError> {
        if let Some(data) = self.data {
            return Ok(data);
        }
        let raw = self.raw_response;
        let request_id = raw.request_id.clone();
        let err = if raw.code != 0 {
            // 传 raw.code 原值(i32),禁止 as u16 截断;分类经 ErrorCode::from_code
            crate::error::api_error(raw.code, "response", raw.msg, request_id.clone())
        } else {
            crate::error::validation_error("response.data", "服务器没有返回有效的数据")
        };
        Err(err.map_context(|ctx| {
            ctx.set_operation("extract_response_data")
                .add_context("resource", context);
            if let Some(req_id) = request_id.as_ref().filter(|r| !r.trim().is_empty()) {
                ctx.set_request_id(req_id);
            }
        }))
    }
}

// 为常见类型实现 ApiResponseTrait
impl ApiResponseTrait for serde_json::Value {}
// String 默认 Data 格式(JSON envelope);Text 请用自定义类型并覆写 data_format + from_text
impl ApiResponseTrait for String {}
impl ApiResponseTrait for Vec<u8> {
    fn data_format() -> ResponseFormat {
        ResponseFormat::Binary
    }

    fn from_binary(_file_name: String, body: Vec<u8>) -> Option<Self> {
        Some(body)
    }
}
impl ApiResponseTrait for () {
    fn requires_payload() -> bool {
        false
    }
}

// 类型别名,用于向后兼容
/// 基础响应类型别名
pub type BaseResponse<T> = Response<T>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_raw_response_default() {
        let response = RawResponse::default();
        assert_eq!(response.code, 0);
        assert_eq!(response.msg, "success");
        assert!(response.request_id.is_none());
        assert!(response.data.is_none());
        assert!(response.error.is_none());
    }

    #[test]
    fn test_raw_response_success() {
        let response = RawResponse::success();
        assert!(response.is_success());
    }

    #[test]
    fn test_raw_response_success_with_data() {
        let data = serde_json::json!({"key": "value"});
        let response = RawResponse::success_with_data(data.clone());
        assert!(response.is_success());
        assert_eq!(response.data, Some(data));
    }

    #[test]
    fn test_raw_response_error() {
        let response = RawResponse::error(400, "Bad Request");
        assert!(!response.is_success());
        assert_eq!(response.code, 400);
        assert!(response.error.is_some());
    }

    #[test]
    fn test_raw_response_get_error() {
        let response = RawResponse::error(404, "Not Found");
        let error = response.get_error();
        assert!(error.is_some());
        assert_eq!(error.unwrap().code, 404);
    }

    #[test]
    fn test_raw_response_serialization() {
        let response = RawResponse::success_with_data(serde_json::json!({"test": 123}));
        let json = serde_json::to_string(&response).unwrap();
        let parsed: RawResponse = serde_json::from_str(&json).expect("JSON 反序列化失败");
        assert!(parsed.is_success());
    }

    #[test]
    fn test_error_info_creation() {
        let error = ErrorInfo {
            code: 500,
            message: "Internal Error".to_string(),
            details: None,
        };
        assert_eq!(error.code, 500);
        assert_eq!(error.message, "Internal Error");
    }

    #[test]
    fn test_response_format() {
        assert_eq!(ResponseFormat::Data, ResponseFormat::Data);
        assert_ne!(ResponseFormat::Data, ResponseFormat::Flatten);
    }

    #[test]
    fn test_response_format_binary() {
        assert_eq!(<Vec<u8>>::data_format(), ResponseFormat::Binary);
    }

    #[test]
    fn test_response_format_default() {
        assert_eq!(<()>::data_format(), ResponseFormat::Data);
    }

    #[test]
    fn test_response_deserialize_requires_raw_response() {
        let payload = r#"{"code":400,"msg":"Bad Request"}"#;
        let parsed = serde_json::from_str::<Response<serde_json::Value>>(payload);
        assert!(parsed.is_err());
    }

    #[test]
    fn test_response_deserialize_with_raw_response_error_keeps_code_and_msg() {
        let payload = r#"{"raw_response":{"code":400,"msg":"Bad Request","request_id":null,"data":null,"error":null},"data":null}"#;
        let parsed = serde_json::from_str::<Response<serde_json::Value>>(payload)
            .expect("JSON 反序列化失败");
        assert_eq!(parsed.raw_response.code, 400);
        assert_eq!(parsed.raw_response.msg, "Bad Request");
        assert!(!parsed.is_success());
    }

    // Response::decode(#486:extract_response_data 收敛到此方法)

    #[test]
    fn decode_returns_data_on_success() {
        let response: Response<String> = Response {
            data: Some("x".to_string()),
            raw_response: RawResponse::success(),
        };
        assert_eq!(response.decode("测试").unwrap(), "x");
    }

    #[test]
    fn decode_missing_data_is_validation_error_with_context() {
        let response: Response<String> = Response {
            data: None,
            raw_response: RawResponse::success(),
        };
        let err = response.decode("测试").expect_err("缺 data 应报错");
        let ctx = err.ctx();
        assert_eq!(ctx.operation(), Some("extract_response_data"));
        assert_eq!(ctx.get_context("resource"), Some("测试"));
    }

    /// 业务错误:保留 msg、request_id;#544 不截断——`raw_code` 原样 + `from_code` 分类 + Display 真码。
    #[test]
    fn decode_business_error_preserves_msg_and_classifies_raw_code() {
        let response: Response<String> = Response {
            data: None,
            raw_response: RawResponse {
                code: 99991663,
                msg: "tenant access token invalid".to_string(),
                request_id: Some("rid-x".to_string()),
                ..RawResponse::success()
            },
        };
        let err = response.decode("测试").expect_err("业务错误应报错");
        assert_eq!(err.ctx().request_id(), Some("rid-x"));
        match err {
            crate::error::CoreError::Api(api) => {
                assert!(
                    api.message.contains("tenant access token invalid"),
                    "msg preserved: {}",
                    api.message
                );
                assert_eq!(
                    api.raw_code, 99991663,
                    "raw_code must preserve full i32 feishu code"
                );
                assert_eq!(
                    api.code,
                    crate::error::ErrorCode::TenantAccessTokenInvalid,
                    "classification must use from_code without u16 truncation"
                );
                let display = api.to_string();
                assert!(
                    display.contains("99991663"),
                    "Display must show real code, not truncated garbage: {display}"
                );
            }
            other => panic!("expected Api for business error, got: {other:?}"),
        }
    }
}