Skip to main content

dingtalk_stream/messages/
frames.rs

1//! 消息帧结构,对齐 Python frames.py
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// 消息头
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct Headers {
9    /// 应用 ID
10    #[serde(rename = "appId", skip_serializing_if = "Option::is_none")]
11    pub app_id: Option<String>,
12    /// 连接 ID
13    #[serde(rename = "connectionId", skip_serializing_if = "Option::is_none")]
14    pub connection_id: Option<String>,
15    /// 内容类型
16    #[serde(rename = "contentType", skip_serializing_if = "Option::is_none")]
17    pub content_type: Option<String>,
18    /// 消息 ID
19    #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
20    pub message_id: Option<String>,
21    /// 时间戳
22    #[serde(rename = "time", skip_serializing_if = "Option::is_none")]
23    pub time: Option<String>,
24    /// 主题
25    #[serde(rename = "topic", skip_serializing_if = "Option::is_none")]
26    pub topic: Option<String>,
27    // Event 专用字段
28    /// 事件产生时间
29    #[serde(rename = "eventBornTime", skip_serializing_if = "Option::is_none")]
30    pub event_born_time: Option<i64>,
31    /// 事件所属企业 ID
32    #[serde(rename = "eventCorpId", skip_serializing_if = "Option::is_none")]
33    pub event_corp_id: Option<String>,
34    /// 事件 ID
35    #[serde(rename = "eventId", skip_serializing_if = "Option::is_none")]
36    pub event_id: Option<String>,
37    /// 事件类型
38    #[serde(rename = "eventType", skip_serializing_if = "Option::is_none")]
39    pub event_type: Option<String>,
40    /// 事件统一应用 ID
41    #[serde(rename = "eventUnifiedAppId", skip_serializing_if = "Option::is_none")]
42    pub event_unified_app_id: Option<String>,
43    /// 扩展字段
44    #[serde(flatten)]
45    pub extensions: HashMap<String, serde_json::Value>,
46}
47
48impl Headers {
49    /// JSON 内容类型常量
50    pub const CONTENT_TYPE_APPLICATION_JSON: &'static str = "application/json";
51}
52
53/// WebSocket 入站消息(使用 serde tag 区分类型)
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(tag = "type")]
56pub enum StreamMessage {
57    /// 事件消息
58    #[serde(rename = "EVENT")]
59    Event(MessageBody),
60    /// 回调消息
61    #[serde(rename = "CALLBACK")]
62    Callback(MessageBody),
63    /// 系统消息
64    #[serde(rename = "SYSTEM")]
65    System(MessageBody),
66}
67
68/// 消息体
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct MessageBody {
71    /// 协议版本
72    #[serde(rename = "specVersion", default)]
73    pub spec_version: String,
74    /// 消息头
75    pub headers: Headers,
76    /// 消息数据(JSON 字符串)
77    #[serde(default)]
78    pub data: String,
79    /// 扩展字段
80    #[serde(flatten)]
81    pub extensions: HashMap<String, serde_json::Value>,
82}
83
84/// ACK 响应消息
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AckMessage {
87    /// 状态码
88    pub code: u16,
89    /// 响应头
90    pub headers: Headers,
91    /// 响应消息
92    #[serde(default)]
93    pub message: String,
94    /// 响应数据(JSON 字符串)
95    #[serde(default)]
96    pub data: String,
97}
98
99impl AckMessage {
100    /// 成功
101    pub const STATUS_OK: u16 = 200;
102    /// 请求错误
103    pub const STATUS_BAD_REQUEST: u16 = 400;
104    /// 未实现
105    pub const STATUS_NOT_IMPLEMENT: u16 = 404;
106    /// 系统异常
107    pub const STATUS_SYSTEM_EXCEPTION: u16 = 500;
108
109    /// 创建一个成功的 ACK 消息
110    pub fn ok(message_id: Option<String>, data: serde_json::Value) -> Self {
111        Self {
112            code: Self::STATUS_OK,
113            headers: Headers {
114                message_id,
115                content_type: Some(Headers::CONTENT_TYPE_APPLICATION_JSON.to_owned()),
116                ..Default::default()
117            },
118            message: "OK".to_owned(),
119            data: serde_json::to_string(&data).unwrap_or_default(),
120        }
121    }
122
123    /// 创建一个未实现的 ACK 消息
124    pub fn not_implemented(message_id: Option<String>) -> Self {
125        Self {
126            code: Self::STATUS_NOT_IMPLEMENT,
127            headers: Headers {
128                message_id,
129                content_type: Some(Headers::CONTENT_TYPE_APPLICATION_JSON.to_owned()),
130                ..Default::default()
131            },
132            message: "not implement".to_owned(),
133            data: String::new(),
134        }
135    }
136}
137
138/// 系统消息常量
139pub struct SystemMessage;
140
141impl SystemMessage {
142    /// 断开连接主题
143    pub const TOPIC_DISCONNECT: &'static str = "disconnect";
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_stream_message_deserialize_event() {
152        let json = r#"{
153            "specVersion": "1.0",
154            "type": "EVENT",
155            "headers": {
156                "appId": "test_app",
157                "messageId": "msg_001",
158                "topic": "/v1.0/im/bot/messages/get",
159                "eventId": "evt_001",
160                "eventType": "chat_update_title",
161                "eventBornTime": 1690106592000,
162                "eventCorpId": "corp_001"
163            },
164            "data": "{\"key\":\"value\"}"
165        }"#;
166        let msg: StreamMessage = serde_json::from_str(json).unwrap();
167        match &msg {
168            StreamMessage::Event(body) => {
169                assert_eq!(body.spec_version, "1.0");
170                assert_eq!(body.headers.app_id.as_deref(), Some("test_app"));
171                assert_eq!(body.headers.event_id.as_deref(), Some("evt_001"));
172                assert_eq!(
173                    body.headers.event_type.as_deref(),
174                    Some("chat_update_title")
175                );
176                assert_eq!(body.headers.event_born_time, Some(1_690_106_592_000));
177                assert_eq!(body.data, r#"{"key":"value"}"#);
178            }
179            _ => panic!("expected Event"),
180        }
181    }
182
183    #[test]
184    fn test_stream_message_deserialize_callback() {
185        let json = r#"{
186            "specVersion": "1.0",
187            "type": "CALLBACK",
188            "headers": {
189                "messageId": "msg_002",
190                "topic": "/v1.0/im/bot/messages/get"
191            },
192            "data": "{\"text\":\"hello\"}"
193        }"#;
194        let msg: StreamMessage = serde_json::from_str(json).unwrap();
195        assert!(matches!(msg, StreamMessage::Callback(_)));
196    }
197
198    #[test]
199    fn test_stream_message_deserialize_system() {
200        let json = r#"{
201            "specVersion": "1.0",
202            "type": "SYSTEM",
203            "headers": {
204                "topic": "disconnect"
205            },
206            "data": ""
207        }"#;
208        let msg: StreamMessage = serde_json::from_str(json).unwrap();
209        match &msg {
210            StreamMessage::System(body) => {
211                assert_eq!(body.headers.topic.as_deref(), Some("disconnect"));
212            }
213            _ => panic!("expected System"),
214        }
215    }
216
217    #[test]
218    fn test_ack_message_serialize() {
219        let ack = AckMessage::ok(
220            Some("msg_001".to_owned()),
221            serde_json::json!({"response": "ok"}),
222        );
223        let json = serde_json::to_value(&ack).unwrap();
224        assert_eq!(json["code"], 200);
225        assert_eq!(json["headers"]["messageId"], "msg_001");
226        assert_eq!(json["headers"]["contentType"], "application/json");
227        assert_eq!(json["message"], "OK");
228    }
229
230    #[test]
231    fn test_ack_not_implemented() {
232        let ack = AckMessage::not_implemented(Some("msg_002".to_owned()));
233        assert_eq!(ack.code, AckMessage::STATUS_NOT_IMPLEMENT);
234        assert_eq!(ack.message, "not implement");
235    }
236
237    #[test]
238    fn test_headers_extensions() {
239        let json = r#"{
240            "appId": "test",
241            "customField": "custom_value"
242        }"#;
243        let headers: Headers = serde_json::from_str(json).unwrap();
244        assert_eq!(headers.app_id.as_deref(), Some("test"));
245        assert_eq!(
246            headers.extensions.get("customField"),
247            Some(&serde_json::Value::String("custom_value".to_owned()))
248        );
249    }
250}