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