1use serde::{Deserialize, Serialize};
4use serde_repr::{Deserialize_repr, Serialize_repr};
5
6pub const ILINK_APP_ID: &str = "bot";
10pub const CHANNEL_VERSION: &str = "2.4.6";
12pub const QR_CODE_BASE_URL: &str = "https://ilinkai.weixin.qq.com/";
14pub const DEFAULT_ILINK_BOT_TYPE: &str = "3";
16pub const STALE_TOKEN_ERRCODE: i32 = -14;
20#[deprecated(
22 since = "0.3.0",
23 note = "use STALE_TOKEN_ERRCODE — -14 means the token is stale, not the session"
24)]
25pub const SESSION_EXPIRED_ERRCODE: i32 = STALE_TOKEN_ERRCODE;
26pub const TEXT_CHUNK_LIMIT: usize = 4000;
28
29pub const DEFAULT_LONG_POLL_TIMEOUT_MS: u64 = 35_000;
33pub const DEFAULT_API_TIMEOUT_MS: u64 = 15_000;
35pub const DEFAULT_CONFIG_TIMEOUT_MS: u64 = 10_000;
37pub const SESSION_PAUSE_DURATION_MS: u64 = 3_600_000;
39pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
41pub const BACKOFF_DELAY_MS: u64 = 30_000;
43pub const RETRY_DELAY_MS: u64 = 2_000;
45pub const UPLOAD_MAX_RETRIES: u32 = 3;
47pub const CONFIG_CACHE_TTL_MS: u64 = 86_400_000;
49pub const MAX_QR_REFRESH_COUNT: u32 = 3;
51pub const DEFAULT_QR_POLL_TIMEOUT_MS: u64 = 35_000;
53
54#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
58#[repr(u8)]
59#[non_exhaustive]
60pub enum UploadMediaType {
61 Image = 1,
63 Video = 2,
65 File = 3,
67 Voice = 4,
69}
70
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum MessageType {
78 #[default]
80 None,
81 User,
83 Bot,
85 Unknown(i32),
87}
88
89impl MessageType {
90 pub fn code(self) -> i32 {
92 match self {
93 Self::None => 0,
94 Self::User => 1,
95 Self::Bot => 2,
96 Self::Unknown(n) => n,
97 }
98 }
99
100 pub fn from_code(code: i32) -> Self {
102 match code {
103 0 => Self::None,
104 1 => Self::User,
105 2 => Self::Bot,
106 n => Self::Unknown(n),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
117#[non_exhaustive]
118pub enum MessageItemType {
119 #[default]
121 None,
122 Text,
124 Image,
126 Voice,
128 File,
130 Video,
132 ToolCallStart,
134 ToolCallResult,
136 Unknown(i32),
138}
139
140impl MessageItemType {
141 pub fn code(self) -> i32 {
143 match self {
144 Self::None => 0,
145 Self::Text => 1,
146 Self::Image => 2,
147 Self::Voice => 3,
148 Self::File => 4,
149 Self::Video => 5,
150 Self::ToolCallStart => 11,
151 Self::ToolCallResult => 12,
152 Self::Unknown(n) => n,
153 }
154 }
155
156 pub fn from_code(code: i32) -> Self {
158 match code {
159 0 => Self::None,
160 1 => Self::Text,
161 2 => Self::Image,
162 3 => Self::Voice,
163 4 => Self::File,
164 5 => Self::Video,
165 11 => Self::ToolCallStart,
166 12 => Self::ToolCallResult,
167 n => Self::Unknown(n),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
177#[non_exhaustive]
178pub enum MessageState {
179 #[default]
181 New,
182 Generating,
184 Finish,
186 Unknown(i32),
188}
189
190impl MessageState {
191 pub fn code(self) -> i32 {
193 match self {
194 Self::New => 0,
195 Self::Generating => 1,
196 Self::Finish => 2,
197 Self::Unknown(n) => n,
198 }
199 }
200
201 pub fn from_code(code: i32) -> Self {
203 match code {
204 0 => Self::New,
205 1 => Self::Generating,
206 2 => Self::Finish,
207 n => Self::Unknown(n),
208 }
209 }
210}
211
212macro_rules! impl_wire_int_serde {
216 ($ty:ty) => {
217 impl Serialize for $ty {
218 fn serialize<S: serde::Serializer>(
219 &self,
220 serializer: S,
221 ) -> std::result::Result<S::Ok, S::Error> {
222 serializer.serialize_i32(self.code())
223 }
224 }
225
226 impl<'de> Deserialize<'de> for $ty {
227 fn deserialize<D: serde::Deserializer<'de>>(
228 deserializer: D,
229 ) -> std::result::Result<Self, D::Error> {
230 Ok(Self::from_code(i32::deserialize(deserializer)?))
231 }
232 }
233 };
234}
235
236impl_wire_int_serde!(MessageType);
237impl_wire_int_serde!(MessageItemType);
238impl_wire_int_serde!(MessageState);
239
240#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
242#[repr(u8)]
243#[non_exhaustive]
244pub enum TypingStatus {
245 Typing = 1,
247 Cancel = 2,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum MediaType {
255 Image,
257 Video,
259 Voice,
261 File,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267#[non_exhaustive]
268pub enum ToolCallStatus {
269 Completed,
271 Failed,
273 Blocked,
275 Unknown,
277}
278
279impl ToolCallStatus {
280 pub fn as_str(self) -> &'static str {
282 match self {
283 Self::Completed => "completed",
284 Self::Failed => "failed",
285 Self::Blocked => "blocked",
286 Self::Unknown => "unknown",
287 }
288 }
289}
290
291#[derive(Debug, Clone, Default, Serialize, Deserialize)]
295pub struct BaseInfo {
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub channel_version: Option<String>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub bot_agent: Option<String>,
302}
303
304pub fn build_base_info_with_agent(bot_agent: &str) -> BaseInfo {
306 BaseInfo {
307 channel_version: Some(CHANNEL_VERSION.to_owned()),
308 bot_agent: Some(bot_agent.to_owned()),
309 }
310}
311
312pub fn build_base_info() -> BaseInfo {
314 build_base_info_with_agent("weixin-agent-rs")
315}
316
317#[derive(Debug, Clone, Default, Serialize, Deserialize)]
321pub struct CdnMedia {
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub encrypt_query_param: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub aes_key: Option<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub encrypt_type: Option<i32>,
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub full_url: Option<String>,
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
338pub struct TextItem {
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub text: Option<String>,
342}
343
344#[derive(Debug, Clone, Default, Serialize, Deserialize)]
346pub struct ImageItem {
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub media: Option<CdnMedia>,
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub thumb_media: Option<CdnMedia>,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub aeskey: Option<String>,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub url: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub mid_size: Option<i64>,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub thumb_size: Option<i64>,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub thumb_height: Option<i64>,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub thumb_width: Option<i64>,
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub hd_size: Option<i64>,
374}
375
376#[derive(Debug, Clone, Default, Serialize, Deserialize)]
378pub struct VoiceItem {
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub media: Option<CdnMedia>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub encode_type: Option<i32>,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub bits_per_sample: Option<i32>,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub sample_rate: Option<i32>,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub playtime: Option<i64>,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub text: Option<String>,
397}
398
399#[derive(Debug, Clone, Default, Serialize, Deserialize)]
401pub struct FileItem {
402 #[serde(skip_serializing_if = "Option::is_none")]
404 pub media: Option<CdnMedia>,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub file_name: Option<String>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub md5: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub len: Option<String>,
414}
415
416#[derive(Debug, Clone, Default, Serialize, Deserialize)]
418pub struct VideoItem {
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub media: Option<CdnMedia>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub video_size: Option<i64>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub play_length: Option<i64>,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub video_md5: Option<String>,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub thumb_media: Option<CdnMedia>,
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub thumb_size: Option<i64>,
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub thumb_height: Option<i64>,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub thumb_width: Option<i64>,
443}
444
445#[derive(Debug, Clone, Default, Serialize, Deserialize)]
447pub struct RefMessage {
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub message_item: Option<Box<MessageItem>>,
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub title: Option<String>,
454}
455
456#[derive(Debug, Clone, Default, Serialize, Deserialize)]
458#[non_exhaustive]
459pub struct ToolCallStartItem {
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub tool_name: Option<String>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub tool_call_id: Option<String>,
466}
467
468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470#[non_exhaustive]
471pub struct ToolCallResultItem {
472 #[serde(skip_serializing_if = "Option::is_none")]
474 pub tool_name: Option<String>,
475 #[serde(skip_serializing_if = "Option::is_none")]
477 pub tool_call_id: Option<String>,
478 #[serde(skip_serializing_if = "Option::is_none")]
480 pub status: Option<String>,
481}
482
483#[derive(Debug, Clone, Default, Serialize, Deserialize)]
485pub struct MessageItem {
486 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
488 pub item_type: Option<MessageItemType>,
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub create_time_ms: Option<i64>,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub update_time_ms: Option<i64>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub is_completed: Option<bool>,
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub msg_id: Option<String>,
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub ref_msg: Option<RefMessage>,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub text_item: Option<TextItem>,
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub image_item: Option<ImageItem>,
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub voice_item: Option<VoiceItem>,
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub file_item: Option<FileItem>,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub video_item: Option<VideoItem>,
519 #[serde(skip_serializing_if = "Option::is_none")]
521 pub tool_call_start_item: Option<ToolCallStartItem>,
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub tool_call_result_item: Option<ToolCallResultItem>,
525}
526
527#[derive(Debug, Clone, Default, Serialize, Deserialize)]
531pub struct WeixinMessage {
532 #[serde(skip_serializing_if = "Option::is_none")]
534 pub seq: Option<i64>,
535 #[serde(skip_serializing_if = "Option::is_none")]
537 pub message_id: Option<i64>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub from_user_id: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub to_user_id: Option<String>,
544 #[serde(skip_serializing_if = "Option::is_none")]
546 pub client_id: Option<String>,
547 #[serde(skip_serializing_if = "Option::is_none")]
549 pub create_time_ms: Option<i64>,
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub update_time_ms: Option<i64>,
553 #[serde(skip_serializing_if = "Option::is_none")]
555 pub delete_time_ms: Option<i64>,
556 #[serde(skip_serializing_if = "Option::is_none")]
558 pub session_id: Option<String>,
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub group_id: Option<String>,
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub message_type: Option<MessageType>,
565 #[serde(skip_serializing_if = "Option::is_none")]
567 pub message_state: Option<MessageState>,
568 #[serde(skip_serializing_if = "Option::is_none")]
570 pub item_list: Option<Vec<MessageItem>>,
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub context_token: Option<String>,
574 #[serde(skip_serializing_if = "Option::is_none")]
576 pub run_id: Option<String>,
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
583pub struct GetUpdatesRequest {
584 pub get_updates_buf: String,
586 pub base_info: BaseInfo,
588}
589
590#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592pub struct GetUpdatesResponse {
593 #[serde(skip_serializing_if = "Option::is_none")]
595 pub ret: Option<i32>,
596 #[serde(skip_serializing_if = "Option::is_none")]
598 pub errcode: Option<i32>,
599 #[serde(skip_serializing_if = "Option::is_none")]
601 pub errmsg: Option<String>,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub msgs: Option<Vec<WeixinMessage>>,
605 #[serde(skip_serializing_if = "Option::is_none")]
607 pub sync_buf: Option<String>,
608 #[serde(skip_serializing_if = "Option::is_none")]
610 pub get_updates_buf: Option<String>,
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub longpolling_timeout_ms: Option<u64>,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct SendMessageRequest {
619 pub msg: WeixinMessage,
621 pub base_info: BaseInfo,
623}
624
625#[derive(Debug, Clone, Default, Serialize, Deserialize)]
627pub(crate) struct SendMessageResponse {
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub ret: Option<i32>,
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub errmsg: Option<String>,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct GetUploadUrlRequest {
639 pub filekey: String,
641 pub media_type: UploadMediaType,
643 pub to_user_id: String,
645 pub rawsize: u64,
647 pub rawfilemd5: String,
649 pub filesize: u64,
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub no_need_thumb: Option<bool>,
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub thumb_rawsize: Option<u64>,
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub thumb_rawfilemd5: Option<String>,
660 #[serde(skip_serializing_if = "Option::is_none")]
662 pub thumb_filesize: Option<u64>,
663 pub aeskey: String,
665 pub base_info: BaseInfo,
667}
668
669#[derive(Debug, Clone, Default, Serialize, Deserialize)]
671pub struct GetUploadUrlResponse {
672 #[serde(skip_serializing_if = "Option::is_none")]
674 pub upload_param: Option<String>,
675 #[serde(skip_serializing_if = "Option::is_none")]
677 pub thumb_upload_param: Option<String>,
678 #[serde(skip_serializing_if = "Option::is_none")]
680 pub upload_full_url: Option<String>,
681}
682
683#[derive(Debug, Clone, Serialize)]
685pub(crate) struct GetConfigRequest {
686 pub ilink_user_id: String,
688 #[serde(skip_serializing_if = "Option::is_none")]
690 pub context_token: Option<String>,
691 pub base_info: BaseInfo,
693}
694
695#[derive(Debug, Clone, Default, Serialize, Deserialize)]
697pub struct GetConfigResponse {
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub ret: Option<i32>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub errmsg: Option<String>,
704 #[serde(skip_serializing_if = "Option::is_none")]
706 pub typing_ticket: Option<String>,
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct SendTypingRequest {
712 pub ilink_user_id: String,
714 #[serde(skip_serializing_if = "Option::is_none")]
716 pub typing_ticket: Option<String>,
717 pub status: TypingStatus,
719 pub base_info: BaseInfo,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize)]
727pub struct QrCodeResponse {
728 pub qrcode: String,
730 pub qrcode_img_content: String,
732}
733
734#[derive(Debug, Clone, Default, Serialize, Deserialize)]
736pub struct QrStatusResponse {
737 pub status: String,
739 #[serde(skip_serializing_if = "Option::is_none")]
741 pub bot_token: Option<String>,
742 #[serde(skip_serializing_if = "Option::is_none")]
744 pub ilink_bot_id: Option<String>,
745 #[serde(skip_serializing_if = "Option::is_none")]
747 pub baseurl: Option<String>,
748 #[serde(skip_serializing_if = "Option::is_none")]
750 pub ilink_user_id: Option<String>,
751 #[serde(skip_serializing_if = "Option::is_none")]
753 pub redirect_host: Option<String>,
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn item_type_round_trips_known_values() {
762 for code in [0, 1, 2, 3, 4, 5, 11, 12] {
763 assert_eq!(MessageItemType::from_code(code).code(), code);
764 }
765 }
766
767 #[test]
768 fn item_type_preserves_unknown_value() {
769 let t = MessageItemType::from_code(99);
770 assert_eq!(t, MessageItemType::Unknown(99));
771 assert_eq!(t.code(), 99);
772 }
773
774 #[test]
775 fn item_type_serializes_as_wire_int() {
776 assert_eq!(
777 serde_json::to_string(&MessageItemType::ToolCallStart).unwrap(),
778 "11"
779 );
780 assert_eq!(
781 serde_json::to_string(&MessageItemType::ToolCallResult).unwrap(),
782 "12"
783 );
784 assert_eq!(
785 serde_json::to_string(&MessageItemType::Unknown(77)).unwrap(),
786 "77"
787 );
788 }
789
790 #[test]
791 fn unknown_item_type_does_not_break_batch() {
792 let json = r#"{"ret":0,"msgs":[
794 {"message_type":1,"from_user_id":"u1","item_list":[{"type":1,"text_item":{"text":"hi"}}]},
795 {"message_type":2,"item_list":[{"type":11,"tool_call_start_item":{"tool_name":"bash"}}]},
796 {"message_type":2,"item_list":[{"type":99}]}
797 ]}"#;
798 let resp: GetUpdatesResponse = serde_json::from_str(json).unwrap();
799 let msgs = resp.msgs.unwrap();
800 assert_eq!(msgs.len(), 3);
801 assert_eq!(
802 msgs[1].item_list.as_ref().unwrap()[0].item_type,
803 Some(MessageItemType::ToolCallStart)
804 );
805 assert_eq!(
806 msgs[2].item_list.as_ref().unwrap()[0].item_type,
807 Some(MessageItemType::Unknown(99))
808 );
809 }
810
811 #[test]
812 fn unknown_message_state_does_not_break_parse() {
813 let json = r#"{"ret":0,"msgs":[{"message_type":1,"message_state":3}]}"#;
814 let resp: GetUpdatesResponse = serde_json::from_str(json).unwrap();
815 assert_eq!(
816 resp.msgs.unwrap()[0].message_state,
817 Some(MessageState::Unknown(3))
818 );
819 }
820
821 #[test]
822 fn unknown_message_type_does_not_break_parse() {
823 let json = r#"{"ret":0,"msgs":[{"message_type":7}]}"#;
824 let resp: GetUpdatesResponse = serde_json::from_str(json).unwrap();
825 assert_eq!(
826 resp.msgs.unwrap()[0].message_type,
827 Some(MessageType::Unknown(7))
828 );
829 }
830
831 #[test]
832 fn run_id_deserializes_and_serializes() {
833 let msg: WeixinMessage = serde_json::from_str(r#"{"run_id":"abc123"}"#).unwrap();
834 assert_eq!(msg.run_id.as_deref(), Some("abc123"));
835 let json = serde_json::to_string(&msg).unwrap();
836 assert!(json.contains(r#""run_id":"abc123""#));
837 let empty = WeixinMessage::default();
839 assert!(!serde_json::to_string(&empty).unwrap().contains("run_id"));
840 }
841
842 #[test]
843 fn tool_call_status_wire_strings() {
844 assert_eq!(ToolCallStatus::Completed.as_str(), "completed");
845 assert_eq!(ToolCallStatus::Failed.as_str(), "failed");
846 assert_eq!(ToolCallStatus::Blocked.as_str(), "blocked");
847 assert_eq!(ToolCallStatus::Unknown.as_str(), "unknown");
848 }
849
850 #[test]
851 fn channel_version_matches_reference() {
852 assert_eq!(CHANNEL_VERSION, "2.4.6");
853 assert_eq!(STALE_TOKEN_ERRCODE, -14);
854 }
855
856 #[test]
857 fn send_message_response_parses_error_and_empty_object() {
858 let err: SendMessageResponse =
859 serde_json::from_str(r#"{"ret":-14,"errmsg":"stale"}"#).unwrap();
860 assert_eq!(err.ret, Some(-14));
861 assert_eq!(err.errmsg.as_deref(), Some("stale"));
862 let ok: SendMessageResponse = serde_json::from_str("{}").unwrap();
863 assert!(ok.ret.is_none());
864 }
865
866 #[test]
867 fn item_type_round_trips_i32_bounds_through_serde() {
868 for code in [i32::MIN, -1, i32::MAX] {
870 let t = MessageItemType::from_code(code);
871 let json = serde_json::to_string(&t).unwrap();
872 assert_eq!(json, code.to_string());
873 assert_eq!(serde_json::from_str::<MessageItemType>(&json).unwrap(), t);
874 }
875 }
876
877 #[test]
878 fn item_type_rejects_out_of_i32_range() {
879 assert!(serde_json::from_str::<MessageItemType>("2147483648").is_err());
881 }
882}