1use super::event_type::EslEventType;
2use super::format::EventFormat;
3use crate::headers::EventHeader;
4use crate::sofia::SofiaEventSubclass;
5use std::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub(crate) enum WireTokenFault {
9 Empty,
10 Newline,
11 Space,
12}
13
14impl WireTokenFault {
15 fn classify(s: &str) -> Option<Self> {
16 if s.is_empty() {
17 Some(Self::Empty)
18 } else if crate::wire_safety::contains_wire_terminator(s) {
19 Some(Self::Newline)
20 } else if s.contains(' ') {
21 Some(Self::Space)
22 } else {
23 None
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct EventSubscriptionError(pub String);
36
37impl fmt::Display for EventSubscriptionError {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 let class = match WireTokenFault::classify(&self.0) {
40 Some(WireTokenFault::Empty) => return f.write_str("event subscription token is empty"),
41 Some(WireTokenFault::Newline) => "a newline",
42 Some(WireTokenFault::Space) => "a space",
43 None => "an unusable character",
44 };
45 write!(
46 f,
47 "event subscription token contains {class} ({} bytes)",
48 self.0
49 .len()
50 )
51 }
52}
53
54impl std::error::Error for EventSubscriptionError {}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct EventSubscription {
85 format: EventFormat,
86 events: Vec<EslEventType>,
87 raw_events: Vec<String>,
88 custom_subclasses: Vec<String>,
89 filters: Vec<(String, String)>,
90}
91
92#[derive(Debug, Clone, Copy)]
94struct WireTokenRules {
95 reject_empty: bool,
96 reject_space: bool,
97}
98
99impl WireTokenRules {
100 const TOKEN: Self = Self {
102 reject_empty: true,
103 reject_space: true,
104 };
105
106 const TEXT: Self = Self {
108 reject_empty: false,
109 reject_space: false,
110 };
111}
112
113fn validate_wire_token(s: &str, rules: WireTokenRules) -> Result<(), EventSubscriptionError> {
114 let rejected = match WireTokenFault::classify(s) {
115 Some(WireTokenFault::Empty) => rules.reject_empty,
116 Some(WireTokenFault::Newline) => true,
117 Some(WireTokenFault::Space) => rules.reject_space,
118 None => false,
119 };
120 if rejected {
121 return Err(EventSubscriptionError(s.to_string()));
122 }
123 Ok(())
124}
125
126fn validate_raw_event(s: &str) -> Result<(), EventSubscriptionError> {
127 validate_wire_token(s, WireTokenRules::TOKEN)
128}
129
130fn validate_custom_subclass(s: &str) -> Result<(), EventSubscriptionError> {
131 validate_wire_token(s, WireTokenRules::TOKEN)
132}
133
134fn validate_filter_field(s: &str) -> Result<(), EventSubscriptionError> {
135 validate_wire_token(s, WireTokenRules::TEXT)
136}
137
138const CUSTOM_TOKEN: &str = "CUSTOM";
139
140fn is_custom_token(name: &str) -> bool {
141 name.eq_ignore_ascii_case(CUSTOM_TOKEN)
142}
143
144pub fn order_event_tokens<'a>(
156 names: impl IntoIterator<Item = &'a str>,
157 subclasses: impl IntoIterator<Item = &'a str>,
158) -> Vec<&'a str> {
159 let mut tokens: Vec<&str> = Vec::new();
160 let mut custom = false;
161
162 for name in names {
163 if is_custom_token(name) {
164 custom = true;
165 } else {
166 tokens.push(name);
167 }
168 }
169
170 let mut subclasses = subclasses
171 .into_iter()
172 .peekable();
173 if custom
174 || subclasses
175 .peek()
176 .is_some()
177 {
178 tokens.push(CUSTOM_TOKEN);
179 tokens.extend(subclasses);
180 }
181
182 tokens
183}
184
185pub fn swallowed_event_types(events: &str) -> Vec<&str> {
205 events
206 .split_whitespace()
207 .skip_while(|t| !is_custom_token(t))
208 .skip(1)
209 .filter(|t| {
210 t.parse::<EslEventType>()
211 .is_ok()
212 })
213 .collect()
214}
215
216impl EventSubscription {
217 pub fn new(format: EventFormat) -> Self {
219 Self {
220 format,
221 events: Vec::new(),
222 raw_events: Vec::new(),
223 custom_subclasses: Vec::new(),
224 filters: Vec::new(),
225 }
226 }
227
228 pub fn all(format: EventFormat) -> Self {
230 Self {
231 format,
232 events: vec![EslEventType::All],
233 raw_events: Vec::new(),
234 custom_subclasses: Vec::new(),
235 filters: Vec::new(),
236 }
237 }
238
239 pub fn event(mut self, event: EslEventType) -> Self {
241 self.events
242 .push(event);
243 self
244 }
245
246 pub fn events<T: IntoIterator<Item = impl std::borrow::Borrow<EslEventType>>>(
248 mut self,
249 events: T,
250 ) -> Self {
251 self.events
252 .extend(
253 events
254 .into_iter()
255 .map(|e| *e.borrow()),
256 );
257 self
258 }
259
260 pub fn event_raw(mut self, event: impl Into<String>) -> Result<Self, EventSubscriptionError> {
269 let s = event.into();
270 validate_raw_event(&s)?;
271 self.raw_events
272 .push(s);
273 Ok(self)
274 }
275
276 pub fn events_raw<I, S>(mut self, events: I) -> Result<Self, EventSubscriptionError>
280 where
281 I: IntoIterator<Item = S>,
282 S: Into<String>,
283 {
284 for e in events {
285 let s = e.into();
286 validate_raw_event(&s)?;
287 self.raw_events
288 .push(s);
289 }
290 Ok(self)
291 }
292
293 pub fn custom_subclass(
297 mut self,
298 subclass: impl Into<String>,
299 ) -> Result<Self, EventSubscriptionError> {
300 let s = subclass.into();
301 validate_custom_subclass(&s)?;
302 self.custom_subclasses
303 .push(s);
304 Ok(self)
305 }
306
307 pub fn custom_subclasses(
311 mut self,
312 subclasses: impl IntoIterator<Item = impl Into<String>>,
313 ) -> Result<Self, EventSubscriptionError> {
314 for s in subclasses {
315 let s = s.into();
316 validate_custom_subclass(&s)?;
317 self.custom_subclasses
318 .push(s);
319 }
320 Ok(self)
321 }
322
323 pub fn sofia_event(mut self, subclass: SofiaEventSubclass) -> Self {
328 self.custom_subclasses
329 .push(
330 subclass
331 .as_str()
332 .to_string(),
333 );
334 self
335 }
336
337 pub fn sofia_events(
339 mut self,
340 subclasses: impl IntoIterator<Item = impl std::borrow::Borrow<SofiaEventSubclass>>,
341 ) -> Self {
342 self.custom_subclasses
343 .extend(
344 subclasses
345 .into_iter()
346 .map(|s| {
347 s.borrow()
348 .as_str()
349 .to_string()
350 }),
351 );
352 self
353 }
354
355 pub fn filter(
359 self,
360 header: EventHeader,
361 value: impl Into<String>,
362 ) -> Result<Self, EventSubscriptionError> {
363 let v = value.into();
364 validate_filter_field(&v)?;
365 let mut s = self;
366 s.filters
367 .push((
368 header
369 .as_str()
370 .to_string(),
371 v,
372 ));
373 Ok(s)
374 }
375
376 pub fn filter_raw(
380 self,
381 header: impl Into<String>,
382 value: impl Into<String>,
383 ) -> Result<Self, EventSubscriptionError> {
384 let h = header.into();
385 let v = value.into();
386 validate_filter_field(&h)?;
387 validate_filter_field(&v)?;
388 let mut s = self;
389 s.filters
390 .push((h, v));
391 Ok(s)
392 }
393
394 pub fn with_format(mut self, format: EventFormat) -> Self {
396 self.format = format;
397 self
398 }
399
400 pub fn format(&self) -> EventFormat {
402 self.format
403 }
404
405 pub fn format_mut(&mut self) -> &mut EventFormat {
407 &mut self.format
408 }
409
410 pub fn event_types(&self) -> &[EslEventType] {
412 &self.events
413 }
414
415 pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
417 &mut self.events
418 }
419
420 pub fn event_types_raw(&self) -> &[String] {
422 &self.raw_events
423 }
424
425 pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
431 &mut self.raw_events
432 }
433
434 pub fn custom_subclass_list(&self) -> &[String] {
436 &self.custom_subclasses
437 }
438
439 pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
441 &mut self.custom_subclasses
442 }
443
444 pub fn filters(&self) -> &[(String, String)] {
446 &self.filters
447 }
448
449 pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
451 &mut self.filters
452 }
453
454 pub fn is_all(&self) -> bool {
456 self.events
457 .contains(&EslEventType::All)
458 }
459
460 pub fn is_empty(&self) -> bool {
463 self.events
464 .is_empty()
465 && self
466 .raw_events
467 .is_empty()
468 && self
469 .custom_subclasses
470 .is_empty()
471 }
472
473 pub fn to_event_string(&self) -> Option<String> {
483 if self
484 .events
485 .contains(&EslEventType::All)
486 {
487 return Some("ALL".to_string());
488 }
489
490 let parts = order_event_tokens(
491 self.events
492 .iter()
493 .map(|e| e.as_str())
494 .chain(
495 self.raw_events
496 .iter()
497 .map(|s| s.as_str()),
498 ),
499 self.custom_subclasses
500 .iter()
501 .map(|s| s.as_str()),
502 );
503
504 if parts.is_empty() {
505 None
506 } else {
507 Some(parts.join(" "))
508 }
509 }
510}
511
512#[cfg(feature = "serde")]
513mod event_subscription_serde {
514 use super::*;
515 use serde::{Deserialize, Serialize};
516
517 #[derive(Serialize, Deserialize)]
518 struct EventSubscriptionRaw {
519 format: EventFormat,
520 #[serde(default)]
521 events: Vec<EslEventType>,
522 #[serde(default, skip_serializing_if = "Vec::is_empty")]
523 raw_events: Vec<String>,
524 #[serde(default)]
525 custom_subclasses: Vec<String>,
526 #[serde(default)]
527 filters: Vec<(String, String)>,
528 }
529
530 impl TryFrom<EventSubscriptionRaw> for EventSubscription {
531 type Error = EventSubscriptionError;
532
533 fn try_from(raw: EventSubscriptionRaw) -> Result<Self, Self::Error> {
534 for re in &raw.raw_events {
535 validate_raw_event(re)?;
536 }
537 for sc in &raw.custom_subclasses {
538 validate_custom_subclass(sc)?;
539 }
540 for (h, v) in &raw.filters {
541 validate_filter_field(h)?;
542 validate_filter_field(v)?;
543 }
544 Ok(EventSubscription {
545 format: raw.format,
546 events: raw.events,
547 raw_events: raw.raw_events,
548 custom_subclasses: raw.custom_subclasses,
549 filters: raw.filters,
550 })
551 }
552 }
553
554 impl Serialize for EventSubscription {
555 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
556 let raw = EventSubscriptionRaw {
557 format: self.format,
558 events: self
559 .events
560 .clone(),
561 raw_events: self
562 .raw_events
563 .clone(),
564 custom_subclasses: self
565 .custom_subclasses
566 .clone(),
567 filters: self
568 .filters
569 .clone(),
570 };
571 raw.serialize(serializer)
572 }
573 }
574
575 impl<'de> Deserialize<'de> for EventSubscription {
576 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
577 let raw = EventSubscriptionRaw::deserialize(deserializer)?;
578 EventSubscription::try_from(raw).map_err(serde::de::Error::custom)
579 }
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn new_creates_empty() {
589 let sub = EventSubscription::new(EventFormat::Plain);
590 assert!(sub.is_empty());
591 assert!(!sub.is_all());
592 assert_eq!(sub.format(), EventFormat::Plain);
593 assert!(sub
594 .event_types()
595 .is_empty());
596 assert!(sub
597 .custom_subclass_list()
598 .is_empty());
599 assert!(sub
600 .filters()
601 .is_empty());
602 }
603
604 #[test]
605 fn all_creates_all() {
606 let sub = EventSubscription::all(EventFormat::Json);
607 assert!(sub.is_all());
608 assert!(!sub.is_empty());
609 assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
610 }
611
612 #[test]
613 fn event_string_typed_only() {
614 let sub = EventSubscription::new(EventFormat::Plain)
615 .event(EslEventType::ChannelCreate)
616 .event(EslEventType::ChannelAnswer);
617 assert_eq!(
618 sub.to_event_string(),
619 Some("CHANNEL_CREATE CHANNEL_ANSWER".to_string())
620 );
621 }
622
623 #[test]
624 fn event_string_custom_only() {
625 let sub = EventSubscription::new(EventFormat::Plain)
626 .custom_subclass("sofia::register")
627 .unwrap()
628 .custom_subclass("sofia::unregister")
629 .unwrap();
630 assert_eq!(
631 sub.to_event_string(),
632 Some("CUSTOM sofia::register sofia::unregister".to_string())
633 );
634 }
635
636 #[test]
637 fn event_string_mixed() {
638 let sub = EventSubscription::new(EventFormat::Plain)
639 .event(EslEventType::Heartbeat)
640 .custom_subclass("sofia::register")
641 .unwrap();
642 assert_eq!(
643 sub.to_event_string(),
644 Some("HEARTBEAT CUSTOM sofia::register".to_string())
645 );
646 }
647
648 #[test]
649 fn event_string_custom_not_duplicated() {
650 let sub = EventSubscription::new(EventFormat::Plain)
651 .event(EslEventType::Custom)
652 .custom_subclass("sofia::register")
653 .unwrap();
654 assert_eq!(
656 sub.to_event_string(),
657 Some("CUSTOM sofia::register".to_string())
658 );
659 }
660
661 #[test]
662 fn order_event_tokens_moves_custom_to_tail() {
663 assert_eq!(
664 order_event_tokens(["CUSTOM", "CHANNEL_CREATE", "HEARTBEAT"], []),
665 ["CHANNEL_CREATE", "HEARTBEAT", "CUSTOM"]
666 );
667 }
668
669 #[test]
670 fn order_event_tokens_without_custom_is_identity() {
671 assert_eq!(
672 order_event_tokens(["CHANNEL_CREATE", "HEARTBEAT"], []),
673 ["CHANNEL_CREATE", "HEARTBEAT"]
674 );
675 }
676
677 #[test]
678 fn order_event_tokens_introduces_custom_for_subclasses() {
679 assert_eq!(
680 order_event_tokens(["HEARTBEAT"], ["sofia::register"]),
681 ["HEARTBEAT", "CUSTOM", "sofia::register"]
682 );
683 }
684
685 #[test]
686 fn order_event_tokens_keeps_bare_custom_without_subclasses() {
687 assert_eq!(order_event_tokens(["CUSTOM"], []), ["CUSTOM"]);
688 }
689
690 #[test]
691 fn order_event_tokens_empty_is_empty() {
692 assert!(order_event_tokens([], []).is_empty());
693 }
694
695 #[test]
696 fn swallowed_event_types_reports_a_type_after_custom() {
697 assert_eq!(
698 swallowed_event_types("HEARTBEAT CUSTOM sofia::register CHANNEL_CREATE"),
699 ["CHANNEL_CREATE"]
700 );
701 }
702
703 #[test]
704 fn swallowed_event_types_empty_when_well_ordered() {
705 assert!(swallowed_event_types("HEARTBEAT CUSTOM sofia::register").is_empty());
706 }
707
708 #[test]
709 fn swallowed_event_types_empty_without_custom() {
710 assert!(swallowed_event_types("HEARTBEAT CHANNEL_CREATE").is_empty());
711 }
712
713 #[test]
714 fn swallowed_event_types_matches_custom_case_insensitively() {
715 assert_eq!(swallowed_event_types("custom HEARTBEAT"), ["HEARTBEAT"]);
716 }
717
718 #[test]
719 fn custom_first_serialises_last() {
720 let sub = EventSubscription::new(EventFormat::Plain)
721 .event(EslEventType::Custom)
722 .event(EslEventType::ChannelCreate)
723 .event(EslEventType::ChannelAnswer);
724 assert_eq!(
725 sub.to_event_string(),
726 Some("CHANNEL_CREATE CHANNEL_ANSWER CUSTOM".to_string())
727 );
728 }
729
730 #[test]
731 fn custom_in_middle_serialises_last() {
732 let sub = EventSubscription::new(EventFormat::Plain)
733 .event(EslEventType::Heartbeat)
734 .event(EslEventType::Custom)
735 .event(EslEventType::ChannelCreate);
736 assert_eq!(
737 sub.to_event_string(),
738 Some("HEARTBEAT CHANNEL_CREATE CUSTOM".to_string())
739 );
740 }
741
742 #[test]
743 fn custom_and_subclasses_follow_every_event_type() {
744 let sub = EventSubscription::new(EventFormat::Plain)
745 .event(EslEventType::Heartbeat)
746 .event(EslEventType::BackgroundJob)
747 .event(EslEventType::Custom)
748 .event(EslEventType::Unpublish)
749 .event(EslEventType::ChannelCreate)
750 .custom_subclass("sofia::gateway_state")
751 .unwrap();
752 assert_eq!(
753 sub.to_event_string(),
754 Some(
755 "HEARTBEAT BACKGROUND_JOB UNPUBLISH CHANNEL_CREATE CUSTOM sofia::gateway_state"
756 .to_string()
757 )
758 );
759 }
760
761 #[test]
762 fn raw_events_order_before_custom() {
763 let sub = EventSubscription::new(EventFormat::Plain)
764 .event(EslEventType::Custom)
765 .event_raw("FUTURE_EVENT")
766 .unwrap()
767 .custom_subclass("sofia::register")
768 .unwrap();
769 assert_eq!(
770 sub.to_event_string(),
771 Some("FUTURE_EVENT CUSTOM sofia::register".to_string())
772 );
773 }
774
775 #[test]
778 fn raw_custom_token_is_the_terminal_marker() {
779 let sub = EventSubscription::new(EventFormat::Plain)
780 .event(EslEventType::Custom)
781 .event_raw("custom")
782 .unwrap()
783 .event_raw("CHANNEL_CREATE")
784 .unwrap();
785 assert_eq!(
786 sub.to_event_string(),
787 Some("CHANNEL_CREATE CUSTOM".to_string())
788 );
789 }
790
791 #[test]
792 fn order_preserved_without_custom() {
793 let sub = EventSubscription::new(EventFormat::Plain)
794 .event(EslEventType::Heartbeat)
795 .event(EslEventType::ChannelAnswer)
796 .event(EslEventType::ChannelCreate)
797 .event(EslEventType::BackgroundJob);
798 assert_eq!(
799 sub.to_event_string(),
800 Some("HEARTBEAT CHANNEL_ANSWER CHANNEL_CREATE BACKGROUND_JOB".to_string())
801 );
802 }
803
804 #[test]
805 fn all_wins_from_any_position() {
806 let sub = EventSubscription::new(EventFormat::Plain)
807 .event(EslEventType::Custom)
808 .event(EslEventType::ChannelCreate)
809 .event(EslEventType::All)
810 .custom_subclass("sofia::register")
811 .unwrap();
812 assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
813 }
814
815 #[test]
820 fn no_requested_event_type_lands_after_custom() {
821 let subs = [
822 EventSubscription::new(EventFormat::Plain)
823 .event(EslEventType::Custom)
824 .event(EslEventType::ChannelCreate),
825 EventSubscription::new(EventFormat::Plain)
826 .event(EslEventType::Heartbeat)
827 .event(EslEventType::Custom)
828 .event(EslEventType::Unpublish)
829 .custom_subclass("sofia::register")
830 .unwrap(),
831 EventSubscription::new(EventFormat::Plain)
832 .events(EslEventType::CHANNEL_EVENTS)
833 .event(EslEventType::Custom)
834 .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS),
835 ];
836
837 for sub in subs {
838 let out = sub
839 .to_event_string()
840 .unwrap();
841 let tokens: Vec<&str> = out
842 .split(' ')
843 .collect();
844 let custom_at = tokens
845 .iter()
846 .position(|t| *t == "CUSTOM")
847 .expect("CUSTOM present");
848 for want in sub.event_types() {
849 if *want == EslEventType::Custom {
850 continue;
851 }
852 let at = tokens
853 .iter()
854 .position(|t| *t == want.as_str())
855 .unwrap_or_else(|| panic!("{} missing from {out:?}", want.as_str()));
856 assert!(
857 at < custom_at,
858 "{} lands after CUSTOM in {out:?}",
859 want.as_str()
860 );
861 }
862 }
863 }
864
865 #[test]
866 fn event_string_empty_is_none() {
867 let sub = EventSubscription::new(EventFormat::Plain);
868 assert_eq!(sub.to_event_string(), None);
869 }
870
871 #[test]
872 fn filters_preserve_order() {
873 let sub = EventSubscription::new(EventFormat::Plain)
874 .filter(EventHeader::CallDirection, "inbound")
875 .unwrap()
876 .filter_raw("X-Custom", "value1")
877 .unwrap()
878 .filter(EventHeader::ChannelState, "CS_EXECUTE")
879 .unwrap();
880 assert_eq!(
881 sub.filters(),
882 &[
883 ("Call-Direction".to_string(), "inbound".to_string()),
884 ("X-Custom".to_string(), "value1".to_string()),
885 ("Channel-State".to_string(), "CS_EXECUTE".to_string()),
886 ]
887 );
888 }
889
890 #[test]
891 fn builder_chain() {
892 let sub = EventSubscription::new(EventFormat::Plain)
893 .events(EslEventType::CHANNEL_EVENTS)
894 .event(EslEventType::Heartbeat)
895 .custom_subclass("sofia::register")
896 .unwrap()
897 .filter(EventHeader::CallDirection, "inbound")
898 .unwrap()
899 .with_format(EventFormat::Json);
900
901 assert_eq!(sub.format(), EventFormat::Json);
902 assert!(!sub.is_empty());
903 assert!(!sub.is_all());
904 assert!(sub
905 .event_types()
906 .contains(&EslEventType::ChannelCreate));
907 assert!(sub
908 .event_types()
909 .contains(&EslEventType::Heartbeat));
910 assert_eq!(sub.custom_subclass_list(), &["sofia::register"]);
911 assert_eq!(
912 sub.filters()
913 .len(),
914 1
915 );
916 }
917
918 #[test]
919 fn serde_round_trip_subscription() {
920 let sub = EventSubscription::new(EventFormat::Plain)
921 .event(EslEventType::ChannelCreate)
922 .event(EslEventType::Heartbeat)
923 .custom_subclass("sofia::register")
924 .unwrap()
925 .filter(EventHeader::CallDirection, "inbound")
926 .unwrap();
927
928 let json = serde_json::to_string(&sub).unwrap();
929 let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
930 assert_eq!(sub, deserialized);
931 }
932
933 #[test]
934 fn serde_rejects_invalid_subclass() {
935 let json =
936 r#"{"format":"Plain","events":[],"custom_subclasses":["bad subclass"],"filters":[]}"#;
937 let result: Result<EventSubscription, _> = serde_json::from_str(json);
938 assert!(result.is_err());
939 let err = result
940 .unwrap_err()
941 .to_string();
942 assert!(err.contains("space"), "error should mention space: {err}");
943 }
944
945 #[test]
946 fn serde_rejects_newline_in_filter() {
947 let json = r#"{"format":"Plain","events":[],"custom_subclasses":[],"filters":[["Header","val\n"]]}"#;
948 let result: Result<EventSubscription, _> = serde_json::from_str(json);
949 assert!(result.is_err());
950 let err = result
951 .unwrap_err()
952 .to_string();
953 assert!(
954 err.contains("newline"),
955 "error should mention newline: {err}"
956 );
957 }
958
959 #[test]
960 fn error_display_names_the_character_class_only() {
961 let err = EventSubscription::new(EventFormat::Plain)
962 .custom_subclass("bad subclass")
963 .unwrap_err();
964 assert_eq!(
965 err.to_string(),
966 "event subscription token contains a space (12 bytes)"
967 );
968 assert_eq!(err.0, "bad subclass");
969
970 let err = EventSubscription::new(EventFormat::Plain)
971 .custom_subclass("")
972 .unwrap_err();
973 assert_eq!(err.to_string(), "event subscription token is empty");
974
975 let err = EventSubscription::new(EventFormat::Plain)
976 .filter_raw("Header", "val\n")
977 .unwrap_err();
978 assert_eq!(
979 err.to_string(),
980 "event subscription token contains a newline (4 bytes)"
981 );
982 assert_eq!(err.0, "val\n");
983 }
984
985 #[test]
986 fn custom_subclass_rejects_space() {
987 let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad subclass");
988 assert!(result.is_err());
989 }
990
991 #[test]
992 fn custom_subclass_rejects_newline() {
993 let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad\nsubclass");
994 assert!(result.is_err());
995 }
996
997 #[test]
998 fn custom_subclass_rejects_empty() {
999 let result = EventSubscription::new(EventFormat::Plain).custom_subclass("");
1000 assert!(result.is_err());
1001 }
1002
1003 #[test]
1004 fn filter_raw_rejects_newline_in_header() {
1005 let result = EventSubscription::new(EventFormat::Plain).filter_raw("Bad\nHeader", "value");
1006 assert!(result.is_err());
1007 }
1008
1009 #[test]
1010 fn filter_raw_rejects_newline_in_value() {
1011 let result = EventSubscription::new(EventFormat::Plain).filter_raw("Header", "bad\nvalue");
1012 assert!(result.is_err());
1013 }
1014
1015 #[test]
1016 fn filter_typed_rejects_newline_in_value() {
1017 let result = EventSubscription::new(EventFormat::Plain)
1018 .filter(EventHeader::CallDirection, "bad\nvalue");
1019 assert!(result.is_err());
1020 }
1021
1022 #[test]
1023 fn sofia_event_single() {
1024 let sub =
1025 EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
1026 assert_eq!(
1027 sub.to_event_string(),
1028 Some("CUSTOM sofia::register".to_string())
1029 );
1030 }
1031
1032 #[test]
1033 fn sofia_events_group() {
1034 let sub = EventSubscription::new(EventFormat::Plain)
1035 .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
1036 let event_str = sub
1037 .to_event_string()
1038 .unwrap();
1039 assert!(event_str.starts_with("CUSTOM"));
1040 assert!(event_str.contains("sofia::gateway_state"));
1041 assert!(event_str.contains("sofia::gateway_add"));
1042 assert!(event_str.contains("sofia::gateway_delete"));
1043 assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
1044 }
1045
1046 #[test]
1047 fn event_raw_wire_string() {
1048 let sub = EventSubscription::new(EventFormat::Plain)
1049 .event(EslEventType::Heartbeat)
1050 .event_raw("NEW_EVENT_NOT_IN_ENUM")
1051 .unwrap();
1052 assert_eq!(
1053 sub.to_event_string(),
1054 Some("HEARTBEAT NEW_EVENT_NOT_IN_ENUM".to_string())
1055 );
1056 }
1057
1058 #[test]
1059 fn events_raw_wire_string() {
1060 let sub = EventSubscription::new(EventFormat::Plain)
1061 .events_raw(["FUTURE_A", "FUTURE_B"])
1062 .unwrap();
1063 assert_eq!(sub.to_event_string(), Some("FUTURE_A FUTURE_B".to_string()));
1064 }
1065
1066 #[test]
1067 fn event_raw_with_custom_subclass() {
1068 let sub = EventSubscription::new(EventFormat::Plain)
1069 .event_raw("NEW_EVENT")
1070 .unwrap()
1071 .custom_subclass("sofia::register")
1072 .unwrap();
1073 assert_eq!(
1074 sub.to_event_string(),
1075 Some("NEW_EVENT CUSTOM sofia::register".to_string())
1076 );
1077 }
1078
1079 #[test]
1080 fn event_raw_rejects_newline() {
1081 assert!(EventSubscription::new(EventFormat::Plain)
1082 .event_raw("bad\nevent")
1083 .is_err());
1084 }
1085
1086 #[test]
1087 fn event_raw_rejects_space() {
1088 assert!(EventSubscription::new(EventFormat::Plain)
1089 .event_raw("bad event")
1090 .is_err());
1091 }
1092
1093 #[test]
1094 fn event_raw_rejects_empty() {
1095 assert!(EventSubscription::new(EventFormat::Plain)
1096 .event_raw("")
1097 .is_err());
1098 }
1099
1100 #[test]
1101 fn events_raw_errors_on_first_invalid() {
1102 let result =
1103 EventSubscription::new(EventFormat::Plain).events_raw(["GOOD", "bad event", "OTHER"]);
1104 assert!(result.is_err());
1105 }
1106
1107 #[test]
1108 fn event_types_raw_mut_mutable() {
1109 let mut sub = EventSubscription::new(EventFormat::Plain);
1110 sub.event_types_raw_mut()
1111 .push("DIRECT_PUSH".to_string());
1112 assert_eq!(sub.event_types_raw(), &["DIRECT_PUSH".to_string()]);
1113 }
1114
1115 #[test]
1116 fn is_empty_sees_raw_events() {
1117 let sub = EventSubscription::new(EventFormat::Plain)
1118 .event_raw("ONLY_RAW")
1119 .unwrap();
1120 assert!(!sub.is_empty());
1121 }
1122
1123 #[test]
1124 fn serde_round_trip_with_raw_events() {
1125 let sub = EventSubscription::new(EventFormat::Plain)
1126 .event(EslEventType::ChannelCreate)
1127 .event_raw("FUTURE_EVENT")
1128 .unwrap()
1129 .custom_subclass("sofia::register")
1130 .unwrap();
1131
1132 let json = serde_json::to_string(&sub).unwrap();
1133 let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
1134 assert_eq!(sub, deserialized);
1135 }
1136
1137 #[test]
1138 fn serde_rejects_invalid_raw_event() {
1139 let json = r#"{"format":"Plain","events":[],"raw_events":["bad event"],"custom_subclasses":[],"filters":[]}"#;
1140 let result: Result<EventSubscription, _> = serde_json::from_str(json);
1141 assert!(result.is_err());
1142 }
1143
1144 #[test]
1145 fn serde_missing_raw_events_field_defaults_to_empty() {
1146 let json =
1149 r#"{"format":"Plain","events":["Heartbeat"],"custom_subclasses":[],"filters":[]}"#;
1150 let sub: EventSubscription = serde_json::from_str(json).unwrap();
1151 assert!(sub
1152 .event_types_raw()
1153 .is_empty());
1154 }
1155
1156 #[test]
1157 fn sofia_event_mixed_with_typed_events() {
1158 let sub = EventSubscription::new(EventFormat::Plain)
1159 .event(EslEventType::Heartbeat)
1160 .sofia_event(SofiaEventSubclass::GatewayState);
1161 assert_eq!(
1162 sub.to_event_string(),
1163 Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
1164 );
1165 }
1166}