1use super::{
2 super::OpCode, DispatchEvent, DispatchEventWithTypeDeserializer, Event, EventConversionError,
3};
4use crate::gateway::payload::incoming::Hello;
5use serde::{
6 de::{
7 value::U8Deserializer, DeserializeSeed, Deserializer, Error as DeError, IgnoredAny,
8 IntoDeserializer, MapAccess, Unexpected, Visitor,
9 },
10 ser::{SerializeStruct, Serializer},
11 Deserialize, Serialize,
12};
13use std::{
14 borrow::Cow,
15 fmt::{Formatter, Result as FmtResult},
16 str::FromStr,
17};
18
19#[derive(Clone, Debug)]
22pub enum GatewayEvent {
23 Dispatch(u64, DispatchEvent),
24 Heartbeat,
25 HeartbeatAck,
26 Hello(Hello),
27 InvalidateSession(bool),
28 Reconnect,
29}
30
31impl TryFrom<Event> for GatewayEvent {
32 type Error = EventConversionError;
33
34 fn try_from(event: Event) -> Result<Self, Self::Error> {
35 Ok(match event {
36 Event::GatewayHeartbeat => Self::Heartbeat,
37 Event::GatewayHeartbeatAck => Self::HeartbeatAck,
38 Event::GatewayHello(v) => Self::Hello(v),
39 Event::GatewayInvalidateSession(v) => Self::InvalidateSession(v),
40 Event::GatewayReconnect => Self::Reconnect,
41
42 _ => return Err(EventConversionError::new(event)),
43 })
44 }
45}
46
47#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
48#[serde(field_identifier, rename_all = "lowercase")]
49enum Field {
50 D,
51 Op,
52 S,
53 T,
54}
55
56#[derive(Debug)]
59pub struct GatewayEventDeserializer<'a> {
60 event_type: Option<Cow<'a, str>>,
61 op: u8,
62 sequence: Option<u64>,
63}
64
65impl<'a> GatewayEventDeserializer<'a> {
66 pub fn new(op: u8, event_type: Option<&'a str>) -> Self {
69 Self {
70 event_type: event_type.map(Into::into),
71 op,
72 sequence: None,
73 }
74 }
75
76 pub fn from_json(input: &'a str) -> Option<Self> {
79 let op = Self::find_opcode(input)?;
80 let event_type = Self::find_event_type(input).map(Into::into);
81 let sequence = Self::find_sequence(input);
82
83 Some(Self {
84 event_type,
85 op,
86 sequence,
87 })
88 }
89
90 pub fn into_owned(self) -> GatewayEventDeserializer<'static> {
95 GatewayEventDeserializer {
96 event_type: self
97 .event_type
98 .map(|event_type| Cow::Owned(event_type.into_owned())),
99 op: self.op,
100 sequence: self.sequence,
101 }
102 }
103
104 #[allow(clippy::missing_const_for_fn)]
106 pub fn into_parts(self) -> (u8, Option<u64>, Option<Cow<'a, str>>) {
107 (self.op, self.sequence, self.event_type)
108 }
109
110 pub fn event_type(&self) -> Option<&str> {
112 self.event_type.as_deref()
113 }
114
115 pub const fn op(&self) -> u8 {
117 self.op
118 }
119
120 pub const fn sequence(&self) -> Option<u64> {
125 self.sequence
126 }
127
128 fn find_event_type(input: &'a str) -> Option<&'a str> {
129 let from = input.find(r#""t":"#)? + 4;
136
137 let start = input.get(from..)?.find(|c: char| !c.is_whitespace())? + from + 1;
141
142 if input.as_bytes().get(start - 1).copied()? != b'"' {
144 return None;
145 }
146
147 let to = input.get(start..)?.find('"')?;
148
149 input.get(start..start + to)
150 }
151
152 fn find_opcode(input: &'a str) -> Option<u8> {
153 Self::find_integer(input, r#""op":"#)
154 }
155
156 fn find_sequence(input: &'a str) -> Option<u64> {
157 Self::find_integer(input, r#""s":"#)
158 }
159
160 fn find_integer<T: FromStr>(input: &'a str, key: &str) -> Option<T> {
161 let from = input.find(key)? + key.len();
167
168 let to = input.get(from..)?.find(&[',', '}'] as &[_])?;
173 let clean = input.get(from..from + to)?.trim();
175
176 T::from_str(clean).ok()
177 }
178}
179
180struct GatewayEventVisitor<'a>(u8, Option<Cow<'a, str>>);
181
182impl GatewayEventVisitor<'_> {
183 fn field<'de, T: Deserialize<'de>, V: MapAccess<'de>>(
184 map: &mut V,
185 field: Field,
186 ) -> Result<T, V::Error> {
187 let mut found = None;
188
189 loop {
190 match map.next_key::<Field>() {
191 Ok(Some(key)) if key == field => found = Some(map.next_value()?),
192 Ok(Some(_)) | Err(_) => {
193 map.next_value::<IgnoredAny>()?;
194
195 continue;
196 }
197 Ok(None) => {
198 break;
199 }
200 }
201 }
202
203 found.ok_or_else(|| {
204 DeError::missing_field(match field {
205 Field::D => "d",
206 Field::Op => "op",
207 Field::S => "s",
208 Field::T => "t",
209 })
210 })
211 }
212
213 fn ignore_all<'de, V: MapAccess<'de>>(map: &mut V) -> Result<(), V::Error> {
214 while let Ok(Some(_)) | Err(_) = map.next_key::<Field>() {
215 map.next_value::<IgnoredAny>()?;
216 }
217
218 Ok(())
219 }
220}
221
222impl<'de> Visitor<'de> for GatewayEventVisitor<'_> {
223 type Value = GatewayEvent;
224
225 fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
226 formatter.write_str("struct GatewayEvent")
227 }
228
229 #[allow(clippy::too_many_lines)]
230 fn visit_map<V>(self, mut map: V) -> Result<GatewayEvent, V::Error>
231 where
232 V: MapAccess<'de>,
233 {
234 static VALID_OPCODES: &[&str] = &[
235 "EVENT",
236 "HEARTBEAT",
237 "HEARTBEAT_ACK",
238 "HELLO",
239 "IDENTIFY",
240 "INVALID_SESSION",
241 "RECONNECT",
242 ];
243
244 let op_deser: U8Deserializer<V::Error> = self.0.into_deserializer();
245
246 let op = OpCode::deserialize(op_deser).ok().ok_or_else(|| {
247 let unexpected = Unexpected::Unsigned(u64::from(self.0));
248
249 DeError::invalid_value(unexpected, &"an opcode")
250 })?;
251
252 Ok(match op {
253 OpCode::Dispatch => {
254 let t = self
255 .1
256 .ok_or_else(|| DeError::custom("event type not provided beforehand"))?;
257
258 let mut d = None;
259 let mut s = None;
260
261 loop {
262 let key = match map.next_key() {
263 Ok(Some(key)) => key,
264 Ok(None) => break,
265 Err(_) => {
266 map.next_value::<IgnoredAny>()?;
267
268 continue;
269 }
270 };
271
272 match key {
273 Field::D => {
274 if d.is_some() {
275 return Err(DeError::duplicate_field("d"));
276 }
277
278 let deserializer = DispatchEventWithTypeDeserializer::new(&t);
279
280 d = Some(map.next_value_seed(deserializer)?);
281 }
282 Field::S => {
283 if s.is_some() {
284 return Err(DeError::duplicate_field("s"));
285 }
286
287 s = Some(map.next_value()?);
288 }
289 Field::Op | Field::T => {
290 map.next_value::<IgnoredAny>()?;
291 }
292 }
293 }
294
295 let d = d.ok_or_else(|| DeError::missing_field("d"))?;
296 let s = s.ok_or_else(|| DeError::missing_field("s"))?;
297
298 GatewayEvent::Dispatch(s, d)
299 }
300 OpCode::Heartbeat => {
301 Self::ignore_all(&mut map)?;
302
303 GatewayEvent::Heartbeat
304 }
305 OpCode::HeartbeatAck => {
306 Self::ignore_all(&mut map)?;
307
308 GatewayEvent::HeartbeatAck
309 }
310 OpCode::Hello => {
311 let hello = Self::field::<Hello, _>(&mut map, Field::D)?;
312
313 Self::ignore_all(&mut map)?;
314
315 GatewayEvent::Hello(hello)
316 }
317 OpCode::InvalidSession => {
318 let invalidate = Self::field::<bool, _>(&mut map, Field::D)?;
319
320 Self::ignore_all(&mut map)?;
321
322 GatewayEvent::InvalidateSession(invalidate)
323 }
324 OpCode::Identify => return Err(DeError::unknown_variant("Identify", VALID_OPCODES)),
325 OpCode::Reconnect => {
326 Self::ignore_all(&mut map)?;
327
328 GatewayEvent::Reconnect
329 }
330 OpCode::RequestGuildMembers => {
331 return Err(DeError::unknown_variant(
332 "RequestGuildMembers",
333 VALID_OPCODES,
334 ))
335 }
336 OpCode::Resume => return Err(DeError::unknown_variant("Resume", VALID_OPCODES)),
337 OpCode::PresenceUpdate => {
338 return Err(DeError::unknown_variant("PresenceUpdate", VALID_OPCODES))
339 }
340 OpCode::VoiceStateUpdate => {
341 return Err(DeError::unknown_variant("VoiceStateUpdate", VALID_OPCODES))
342 }
343 })
344 }
345}
346
347impl<'de> DeserializeSeed<'de> for GatewayEventDeserializer<'_> {
348 type Value = GatewayEvent;
349
350 fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
351 const FIELDS: &[&str] = &["op", "d", "s", "t"];
352
353 deserializer.deserialize_struct(
354 "GatewayEvent",
355 FIELDS,
356 GatewayEventVisitor(self.op, self.event_type),
357 )
358 }
359}
360
361impl Serialize for GatewayEvent {
362 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
363 const fn opcode(gateway_event: &GatewayEvent) -> OpCode {
364 match gateway_event {
365 GatewayEvent::Dispatch(_, _) => OpCode::Dispatch,
366 GatewayEvent::Heartbeat => OpCode::Heartbeat,
367 GatewayEvent::HeartbeatAck => OpCode::HeartbeatAck,
368 GatewayEvent::Hello(_) => OpCode::Hello,
369 GatewayEvent::InvalidateSession(_) => OpCode::InvalidSession,
370 GatewayEvent::Reconnect => OpCode::Reconnect,
371 }
372 }
373
374 let mut s = serializer.serialize_struct("GatewayEvent", 4)?;
375
376 if let Self::Dispatch(sequence, event) = self {
377 s.serialize_field("t", &event.kind())?;
378 s.serialize_field("s", &sequence)?;
379 s.serialize_field("op", &opcode(self))?;
380 s.serialize_field("d", &event)?;
381
382 return s.end();
383 }
384
385 s.serialize_field("t", &None::<&str>)?;
387 s.serialize_field("s", &None::<u64>)?;
388 s.serialize_field("op", &opcode(self))?;
389
390 match self {
391 Self::Dispatch(_, _) => unreachable!("dispatch already handled"),
392 Self::Hello(hello) => {
393 s.serialize_field("d", &hello)?;
394 }
395 Self::InvalidateSession(invalidate) => {
396 s.serialize_field("d", &invalidate)?;
397 }
398 Self::Heartbeat | Self::HeartbeatAck | Self::Reconnect => {
399 s.serialize_field("d", &None::<u64>)?;
400 }
401 }
402
403 s.end()
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::{DispatchEvent, GatewayEvent, GatewayEventDeserializer, OpCode};
410 use crate::{
411 gateway::payload::incoming::{Hello, RoleDelete},
412 id::Id,
413 test::image_hash,
414 };
415 use serde::de::DeserializeSeed;
416 use serde_json::de::Deserializer;
417 use serde_test::Token;
418
419 #[test]
420 fn deserialize_dispatch_role_delete() {
421 let input = r#"{
422 "d": {
423 "guild_id": "1",
424 "role_id": "2"
425 },
426 "op": 0,
427 "s": 7,
428 "t": "GUILD_ROLE_DELETE"
429 }"#;
430
431 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
432 let mut json_deserializer = Deserializer::from_str(input);
433 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
434 assert!(matches!(event, GatewayEvent::Dispatch(7, _)));
435 }
436
437 #[test]
438 fn deserialize_dispatch_guild_update() {
439 let input = format!(
440 r#"{{
441 "d": {{
442 "afk_channel_id": "1337",
443 "afk_timeout": 300,
444 "application_id": null,
445 "banner": null,
446 "default_message_notifications": 0,
447 "description": null,
448 "discovery_splash": null,
449 "emojis": [
450 {{
451 "animated": false,
452 "available": true,
453 "id": "1338",
454 "managed": false,
455 "name": "goodboi",
456 "require_colons": true,
457 "roles": []
458 }}
459 ],
460 "explicit_content_filter": 0,
461 "features": [
462 "INVITE_SPLASH",
463 "ANIMATED_ICON"
464 ],
465 "guild_id": "1339",
466 "icon": "{icon}",
467 "id": "13310",
468 "max_members": 250000,
469 "max_presences": null,
470 "mfa_level": 0,
471 "name": "FooBaz",
472 "nsfw_level": 1,
473 "owner_id": "13311",
474 "preferred_locale": "en-US",
475 "premium_progress_bar_enabled": true,
476 "premium_subscription_count": 4,
477 "premium_tier": 1,
478 "region": "eu-central",
479 "roles": [
480 {{
481 "color": 0,
482 "hoist": false,
483 "id": "13312",
484 "managed": false,
485 "mentionable": false,
486 "name": "@everyone",
487 "permissions": "104193601",
488 "position": 0,
489 "flags": 0
490 }}
491 ],
492 "rules_channel_id": null,
493 "splash": "{splash}",
494 "system_channel_flags": 0,
495 "system_channel_id": "13313",
496 "vanity_url_code": null,
497 "verification_level": 0,
498 "widget_channel_id": null,
499 "widget_enabled": false
500 }},
501 "op": 0,
502 "s": 42,
503 "t": "GUILD_UPDATE"
504}}"#,
505 icon = image_hash::ICON_INPUT,
506 splash = image_hash::SPLASH_INPUT,
507 );
508
509 let deserializer = GatewayEventDeserializer::from_json(&input).unwrap();
510 let mut json_deserializer = Deserializer::from_str(&input);
511 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
512
513 assert!(matches!(event, GatewayEvent::Dispatch(42, _)));
514 }
515
516 #[test]
517 fn deserialize_dispatch_guild_update_2() {
518 let input = format!(
519 r#"{{
520 "d": {{
521 "afk_channel_id": null,
522 "afk_timeout": 300,
523 "application_id": null,
524 "banner": null,
525 "default_message_notifications": 0,
526 "description": null,
527 "discovery_splash": null,
528 "emojis": [
529 {{
530 "animated": false,
531 "available": true,
532 "id": "42",
533 "managed": false,
534 "name": "emmet",
535 "require_colons": true,
536 "roles": []
537 }}
538 ],
539 "explicit_content_filter": 2,
540 "features": [],
541 "guild_id": "43",
542 "icon": "{icon}",
543 "id": "45",
544 "max_members": 250000,
545 "max_presences": null,
546 "mfa_level": 0,
547 "name": "FooBar",
548 "nsfw_level": 0,
549 "owner_id": "46",
550 "preferred_locale": "en-US",
551 "premium_progress_bar_enabled": false,
552 "premium_subscription_count": null,
553 "premium_tier": 0,
554 "region": "us-central",
555 "roles": [
556 {{
557 "color": 0,
558 "hoist": false,
559 "id": "47",
560 "managed": false,
561 "mentionable": false,
562 "name": "@everyone",
563 "permissions": "104324673",
564 "position": 0,
565 "flags": 0
566 }}
567 ],
568 "rules_channel_id": null,
569 "splash": null,
570 "system_channel_flags": 0,
571 "system_channel_id": "48",
572 "vanity_url_code": null,
573 "verification_level": 4,
574 "widget_channel_id": null,
575 "widget_enabled": true
576 }},
577 "op": 0,
578 "s": 1190911,
579 "t": "GUILD_UPDATE"
580}}"#,
581 icon = image_hash::ICON_INPUT
582 );
583
584 let deserializer = GatewayEventDeserializer::from_json(&input).unwrap();
585 let mut json_deserializer = Deserializer::from_str(&input);
586 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
587
588 assert!(matches!(event, GatewayEvent::Dispatch(1_190_911, _)));
589 }
590
591 #[test]
594 fn deserialize_dispatch_resumed() {
595 let input = r#"{
596 "t": "RESUMED",
597 "s": 37448,
598 "op": 0,
599 "d": {
600 "_trace": [
601 "[\"gateway-prd-main-zqnl\",{\"micros\":11488,\"calls\":[\"discord-sessions-prd-1-38\",{\"micros\":1756}]}]"
602 ]
603 }
604}"#;
605
606 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
607 let mut json_deserializer = Deserializer::from_str(input);
608 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
609
610 assert!(matches!(event, GatewayEvent::Dispatch(_, _)));
611 }
612
613 #[test]
614 fn deserialize_heartbeat() {
615 let input = r#"{
616 "t": null,
617 "s": null,
618 "op": 1,
619 "d": null
620 }"#;
621
622 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
623 let mut json_deserializer = Deserializer::from_str(input);
624 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
625
626 assert!(matches!(event, GatewayEvent::Heartbeat));
627 }
628
629 #[test]
630 fn deserialize_heartbeat_ack() {
631 let input = r#"{
632 "t": null,
633 "s": null,
634 "op": 11,
635 "d": null
636 }"#;
637
638 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
639 let mut json_deserializer = Deserializer::from_str(input);
640 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
641
642 assert!(matches!(event, GatewayEvent::HeartbeatAck));
643 }
644
645 #[test]
646 fn deserialize_hello() {
647 let input = r#"{
648 "t": null,
649 "s": null,
650 "op": 10,
651 "d": {
652 "heartbeat_interval": 41250,
653 "_trace": [
654 "[\"gateway-prd-main-mjmw\",{\"micros\":0.0}]"
655 ]
656 }
657 }"#;
658
659 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
660 let mut json_deserializer = Deserializer::from_str(input);
661 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
662
663 assert!(matches!(
664 event,
665 GatewayEvent::Hello(Hello {
666 heartbeat_interval: 41_250
667 })
668 ));
669 }
670
671 #[test]
672 fn deserialize_invalidate_session() {
673 let input = r#"{
674 "t": null,
675 "s": null,
676 "op": 9,
677 "d": true
678 }"#;
679
680 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
681 let mut json_deserializer = Deserializer::from_str(input);
682 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
683
684 assert!(matches!(event, GatewayEvent::InvalidateSession(true)));
685 }
686
687 #[test]
688 fn deserialize_reconnect() {
689 let input = r#"{
690 "t": null,
691 "s": null,
692 "op": 7,
693 "d": null
694 }"#;
695
696 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
697 let mut json_deserializer = Deserializer::from_str(input);
698 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
699
700 assert!(matches!(event, GatewayEvent::Reconnect));
701 }
702
703 #[test]
706 fn deserializer_from_json_nested_quotes() {
707 let input = r#"{
708 "t": "DOESNT_MATTER",
709 "s": 5144,
710 "op": 0,
711 "d": {
712 "name": "a \"t\"role"
713 }
714 }"#;
715
716 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
717 assert_eq!(deserializer.event_type(), Some("DOESNT_MATTER"));
718 assert_eq!(deserializer.op, 0);
719 }
720
721 #[allow(unused)]
724 #[test]
725 fn deserializer_handles_null_event_types() {
726 let input = r#"{"t":null,"op":11}"#;
727
728 let deserializer = GatewayEventDeserializer::from_json(input).unwrap();
729 let mut json_deserializer = Deserializer::from_str(input);
730 let event = deserializer.deserialize(&mut json_deserializer).unwrap();
731
732 assert!(matches!(event, GatewayEvent::HeartbeatAck));
733 }
734
735 #[test]
736 fn serialize_dispatch() {
737 let role_delete = RoleDelete {
738 guild_id: Id::new(1),
739 role_id: Id::new(2),
740 };
741 let value = GatewayEvent::Dispatch(2_048, DispatchEvent::RoleDelete(role_delete));
742
743 serde_test::assert_ser_tokens(
744 &value,
745 &[
746 Token::Struct {
747 name: "GatewayEvent",
748 len: 4,
749 },
750 Token::Str("t"),
751 Token::UnitVariant {
752 name: "EventType",
753 variant: "GUILD_ROLE_DELETE",
754 },
755 Token::Str("s"),
756 Token::U64(2_048),
757 Token::Str("op"),
758 Token::U8(OpCode::Dispatch as u8),
759 Token::Str("d"),
760 Token::Struct {
761 name: "RoleDelete",
762 len: 2,
763 },
764 Token::Str("guild_id"),
765 Token::NewtypeStruct { name: "Id" },
766 Token::Str("1"),
767 Token::Str("role_id"),
768 Token::NewtypeStruct { name: "Id" },
769 Token::Str("2"),
770 Token::StructEnd,
771 Token::StructEnd,
772 ],
773 );
774 }
775
776 #[test]
777 fn serialize_heartbeat() {
778 serde_test::assert_ser_tokens(
779 &GatewayEvent::Heartbeat,
780 &[
781 Token::Struct {
782 name: "GatewayEvent",
783 len: 4,
784 },
785 Token::Str("t"),
786 Token::None,
787 Token::Str("s"),
788 Token::None,
789 Token::Str("op"),
790 Token::U8(OpCode::Heartbeat as u8),
791 Token::Str("d"),
792 Token::None,
793 Token::StructEnd,
794 ],
795 );
796 }
797
798 #[test]
799 fn serialize_heartbeat_ack() {
800 serde_test::assert_ser_tokens(
801 &GatewayEvent::HeartbeatAck,
802 &[
803 Token::Struct {
804 name: "GatewayEvent",
805 len: 4,
806 },
807 Token::Str("t"),
808 Token::None,
809 Token::Str("s"),
810 Token::None,
811 Token::Str("op"),
812 Token::U8(OpCode::HeartbeatAck as u8),
813 Token::Str("d"),
814 Token::None,
815 Token::StructEnd,
816 ],
817 );
818 }
819
820 #[test]
821 fn serialize_hello() {
822 serde_test::assert_ser_tokens(
823 &GatewayEvent::Hello(Hello {
824 heartbeat_interval: 41250,
825 }),
826 &[
827 Token::Struct {
828 name: "GatewayEvent",
829 len: 4,
830 },
831 Token::Str("t"),
832 Token::None,
833 Token::Str("s"),
834 Token::None,
835 Token::Str("op"),
836 Token::U8(OpCode::Hello as u8),
837 Token::Str("d"),
838 Token::Struct {
839 name: "Hello",
840 len: 1,
841 },
842 Token::Str("heartbeat_interval"),
843 Token::U64(41250),
844 Token::StructEnd,
845 Token::StructEnd,
846 ],
847 );
848 }
849
850 #[test]
851 fn serialize_invalidate() {
852 let value = GatewayEvent::InvalidateSession(true);
853
854 serde_test::assert_ser_tokens(
855 &value,
856 &[
857 Token::Struct {
858 name: "GatewayEvent",
859 len: 4,
860 },
861 Token::Str("t"),
862 Token::None,
863 Token::Str("s"),
864 Token::None,
865 Token::Str("op"),
866 Token::U8(OpCode::InvalidSession as u8),
867 Token::Str("d"),
868 Token::Bool(true),
869 Token::StructEnd,
870 ],
871 );
872 }
873
874 #[test]
875 fn serialize_reconnect() {
876 serde_test::assert_ser_tokens(
877 &GatewayEvent::Reconnect,
878 &[
879 Token::Struct {
880 name: "GatewayEvent",
881 len: 4,
882 },
883 Token::Str("t"),
884 Token::None,
885 Token::Str("s"),
886 Token::None,
887 Token::Str("op"),
888 Token::U8(OpCode::Reconnect as u8),
889 Token::Str("d"),
890 Token::None,
891 Token::StructEnd,
892 ],
893 );
894 }
895}