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, PartialEq, Eq)]
12pub struct EventSubscriptionError(pub String);
13
14impl fmt::Display for EventSubscriptionError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(f, "invalid event subscription: {}", self.0)
17 }
18}
19
20impl std::error::Error for EventSubscriptionError {}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct EventSubscription {
52 format: EventFormat,
53 events: Vec<EslEventType>,
54 raw_events: Vec<String>,
55 custom_subclasses: Vec<String>,
56 filters: Vec<(String, String)>,
57}
58
59fn validate_wire_token(
65 s: &str,
66 label: &str,
67 reject_empty: bool,
68 reject_space: bool,
69) -> Result<(), EventSubscriptionError> {
70 if reject_empty && s.is_empty() {
71 return Err(EventSubscriptionError(format!("{} cannot be empty", label)));
72 }
73 if crate::wire_safety::contains_wire_terminator(s) {
74 return Err(EventSubscriptionError(format!(
75 "{} contains newline: {:?}",
76 label, s
77 )));
78 }
79 if reject_space && s.contains(' ') {
80 return Err(EventSubscriptionError(format!(
81 "{} contains space: {:?}",
82 label, s
83 )));
84 }
85 Ok(())
86}
87
88fn validate_raw_event(s: &str) -> Result<(), EventSubscriptionError> {
89 validate_wire_token(s, "raw event", true, true)
90}
91
92fn validate_custom_subclass(s: &str) -> Result<(), EventSubscriptionError> {
93 validate_wire_token(s, "custom subclass", true, true)
94}
95
96fn validate_filter_field(field: &str, label: &str) -> Result<(), EventSubscriptionError> {
97 validate_wire_token(field, &format!("filter {}", label), false, false)
98}
99
100const CUSTOM_TOKEN: &str = "CUSTOM";
101
102fn is_custom_token(name: &str) -> bool {
103 name.eq_ignore_ascii_case(CUSTOM_TOKEN)
104}
105
106pub fn order_event_tokens<'a>(
118 names: impl IntoIterator<Item = &'a str>,
119 subclasses: impl IntoIterator<Item = &'a str>,
120) -> Vec<&'a str> {
121 let mut tokens: Vec<&str> = Vec::new();
122 let mut custom = false;
123
124 for name in names {
125 if is_custom_token(name) {
126 custom = true;
127 } else {
128 tokens.push(name);
129 }
130 }
131
132 let mut subclasses = subclasses
133 .into_iter()
134 .peekable();
135 if custom
136 || subclasses
137 .peek()
138 .is_some()
139 {
140 tokens.push(CUSTOM_TOKEN);
141 tokens.extend(subclasses);
142 }
143
144 tokens
145}
146
147pub fn swallowed_event_types(events: &str) -> Vec<&str> {
167 events
168 .split_whitespace()
169 .skip_while(|t| !is_custom_token(t))
170 .skip(1)
171 .filter(|t| {
172 t.parse::<EslEventType>()
173 .is_ok()
174 })
175 .collect()
176}
177
178impl EventSubscription {
179 pub fn new(format: EventFormat) -> Self {
181 Self {
182 format,
183 events: Vec::new(),
184 raw_events: Vec::new(),
185 custom_subclasses: Vec::new(),
186 filters: Vec::new(),
187 }
188 }
189
190 pub fn all(format: EventFormat) -> Self {
192 Self {
193 format,
194 events: vec![EslEventType::All],
195 raw_events: Vec::new(),
196 custom_subclasses: Vec::new(),
197 filters: Vec::new(),
198 }
199 }
200
201 pub fn event(mut self, event: EslEventType) -> Self {
203 self.events
204 .push(event);
205 self
206 }
207
208 pub fn events<T: IntoIterator<Item = impl std::borrow::Borrow<EslEventType>>>(
210 mut self,
211 events: T,
212 ) -> Self {
213 self.events
214 .extend(
215 events
216 .into_iter()
217 .map(|e| *e.borrow()),
218 );
219 self
220 }
221
222 pub fn event_raw(mut self, event: impl Into<String>) -> Result<Self, EventSubscriptionError> {
231 let s = event.into();
232 validate_raw_event(&s)?;
233 self.raw_events
234 .push(s);
235 Ok(self)
236 }
237
238 pub fn events_raw<I, S>(mut self, events: I) -> Result<Self, EventSubscriptionError>
242 where
243 I: IntoIterator<Item = S>,
244 S: Into<String>,
245 {
246 for e in events {
247 let s = e.into();
248 validate_raw_event(&s)?;
249 self.raw_events
250 .push(s);
251 }
252 Ok(self)
253 }
254
255 pub fn custom_subclass(
259 mut self,
260 subclass: impl Into<String>,
261 ) -> Result<Self, EventSubscriptionError> {
262 let s = subclass.into();
263 validate_custom_subclass(&s)?;
264 self.custom_subclasses
265 .push(s);
266 Ok(self)
267 }
268
269 pub fn custom_subclasses(
273 mut self,
274 subclasses: impl IntoIterator<Item = impl Into<String>>,
275 ) -> Result<Self, EventSubscriptionError> {
276 for s in subclasses {
277 let s = s.into();
278 validate_custom_subclass(&s)?;
279 self.custom_subclasses
280 .push(s);
281 }
282 Ok(self)
283 }
284
285 pub fn sofia_event(mut self, subclass: SofiaEventSubclass) -> Self {
290 self.custom_subclasses
291 .push(
292 subclass
293 .as_str()
294 .to_string(),
295 );
296 self
297 }
298
299 pub fn sofia_events(
301 mut self,
302 subclasses: impl IntoIterator<Item = impl std::borrow::Borrow<SofiaEventSubclass>>,
303 ) -> Self {
304 self.custom_subclasses
305 .extend(
306 subclasses
307 .into_iter()
308 .map(|s| {
309 s.borrow()
310 .as_str()
311 .to_string()
312 }),
313 );
314 self
315 }
316
317 pub fn filter(
321 self,
322 header: EventHeader,
323 value: impl Into<String>,
324 ) -> Result<Self, EventSubscriptionError> {
325 let v = value.into();
326 validate_filter_field(&v, "value")?;
327 let mut s = self;
328 s.filters
329 .push((
330 header
331 .as_str()
332 .to_string(),
333 v,
334 ));
335 Ok(s)
336 }
337
338 pub fn filter_raw(
342 self,
343 header: impl Into<String>,
344 value: impl Into<String>,
345 ) -> Result<Self, EventSubscriptionError> {
346 let h = header.into();
347 let v = value.into();
348 validate_filter_field(&h, "header")?;
349 validate_filter_field(&v, "value")?;
350 let mut s = self;
351 s.filters
352 .push((h, v));
353 Ok(s)
354 }
355
356 pub fn with_format(mut self, format: EventFormat) -> Self {
358 self.format = format;
359 self
360 }
361
362 pub fn format(&self) -> EventFormat {
364 self.format
365 }
366
367 pub fn format_mut(&mut self) -> &mut EventFormat {
369 &mut self.format
370 }
371
372 pub fn event_types(&self) -> &[EslEventType] {
374 &self.events
375 }
376
377 pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
379 &mut self.events
380 }
381
382 pub fn event_types_raw(&self) -> &[String] {
384 &self.raw_events
385 }
386
387 pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
393 &mut self.raw_events
394 }
395
396 pub fn custom_subclass_list(&self) -> &[String] {
398 &self.custom_subclasses
399 }
400
401 pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
403 &mut self.custom_subclasses
404 }
405
406 pub fn filters(&self) -> &[(String, String)] {
408 &self.filters
409 }
410
411 pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
413 &mut self.filters
414 }
415
416 pub fn is_all(&self) -> bool {
418 self.events
419 .contains(&EslEventType::All)
420 }
421
422 pub fn is_empty(&self) -> bool {
425 self.events
426 .is_empty()
427 && self
428 .raw_events
429 .is_empty()
430 && self
431 .custom_subclasses
432 .is_empty()
433 }
434
435 pub fn to_event_string(&self) -> Option<String> {
445 if self
446 .events
447 .contains(&EslEventType::All)
448 {
449 return Some("ALL".to_string());
450 }
451
452 let parts = order_event_tokens(
453 self.events
454 .iter()
455 .map(|e| e.as_str())
456 .chain(
457 self.raw_events
458 .iter()
459 .map(|s| s.as_str()),
460 ),
461 self.custom_subclasses
462 .iter()
463 .map(|s| s.as_str()),
464 );
465
466 if parts.is_empty() {
467 None
468 } else {
469 Some(parts.join(" "))
470 }
471 }
472}
473
474#[cfg(feature = "serde")]
475mod event_subscription_serde {
476 use super::*;
477 use serde::{Deserialize, Serialize};
478
479 #[derive(Serialize, Deserialize)]
480 struct EventSubscriptionRaw {
481 format: EventFormat,
482 #[serde(default)]
483 events: Vec<EslEventType>,
484 #[serde(default, skip_serializing_if = "Vec::is_empty")]
485 raw_events: Vec<String>,
486 #[serde(default)]
487 custom_subclasses: Vec<String>,
488 #[serde(default)]
489 filters: Vec<(String, String)>,
490 }
491
492 impl TryFrom<EventSubscriptionRaw> for EventSubscription {
493 type Error = EventSubscriptionError;
494
495 fn try_from(raw: EventSubscriptionRaw) -> Result<Self, Self::Error> {
496 for re in &raw.raw_events {
497 validate_raw_event(re)?;
498 }
499 for sc in &raw.custom_subclasses {
500 validate_custom_subclass(sc)?;
501 }
502 for (h, v) in &raw.filters {
503 validate_filter_field(h, "header")?;
504 validate_filter_field(v, "value")?;
505 }
506 Ok(EventSubscription {
507 format: raw.format,
508 events: raw.events,
509 raw_events: raw.raw_events,
510 custom_subclasses: raw.custom_subclasses,
511 filters: raw.filters,
512 })
513 }
514 }
515
516 impl Serialize for EventSubscription {
517 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
518 let raw = EventSubscriptionRaw {
519 format: self.format,
520 events: self
521 .events
522 .clone(),
523 raw_events: self
524 .raw_events
525 .clone(),
526 custom_subclasses: self
527 .custom_subclasses
528 .clone(),
529 filters: self
530 .filters
531 .clone(),
532 };
533 raw.serialize(serializer)
534 }
535 }
536
537 impl<'de> Deserialize<'de> for EventSubscription {
538 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
539 let raw = EventSubscriptionRaw::deserialize(deserializer)?;
540 EventSubscription::try_from(raw).map_err(serde::de::Error::custom)
541 }
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn new_creates_empty() {
551 let sub = EventSubscription::new(EventFormat::Plain);
552 assert!(sub.is_empty());
553 assert!(!sub.is_all());
554 assert_eq!(sub.format(), EventFormat::Plain);
555 assert!(sub
556 .event_types()
557 .is_empty());
558 assert!(sub
559 .custom_subclass_list()
560 .is_empty());
561 assert!(sub
562 .filters()
563 .is_empty());
564 }
565
566 #[test]
567 fn all_creates_all() {
568 let sub = EventSubscription::all(EventFormat::Json);
569 assert!(sub.is_all());
570 assert!(!sub.is_empty());
571 assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
572 }
573
574 #[test]
575 fn event_string_typed_only() {
576 let sub = EventSubscription::new(EventFormat::Plain)
577 .event(EslEventType::ChannelCreate)
578 .event(EslEventType::ChannelAnswer);
579 assert_eq!(
580 sub.to_event_string(),
581 Some("CHANNEL_CREATE CHANNEL_ANSWER".to_string())
582 );
583 }
584
585 #[test]
586 fn event_string_custom_only() {
587 let sub = EventSubscription::new(EventFormat::Plain)
588 .custom_subclass("sofia::register")
589 .unwrap()
590 .custom_subclass("sofia::unregister")
591 .unwrap();
592 assert_eq!(
593 sub.to_event_string(),
594 Some("CUSTOM sofia::register sofia::unregister".to_string())
595 );
596 }
597
598 #[test]
599 fn event_string_mixed() {
600 let sub = EventSubscription::new(EventFormat::Plain)
601 .event(EslEventType::Heartbeat)
602 .custom_subclass("sofia::register")
603 .unwrap();
604 assert_eq!(
605 sub.to_event_string(),
606 Some("HEARTBEAT CUSTOM sofia::register".to_string())
607 );
608 }
609
610 #[test]
611 fn event_string_custom_not_duplicated() {
612 let sub = EventSubscription::new(EventFormat::Plain)
613 .event(EslEventType::Custom)
614 .custom_subclass("sofia::register")
615 .unwrap();
616 assert_eq!(
618 sub.to_event_string(),
619 Some("CUSTOM sofia::register".to_string())
620 );
621 }
622
623 #[test]
624 fn order_event_tokens_moves_custom_to_tail() {
625 assert_eq!(
626 order_event_tokens(["CUSTOM", "CHANNEL_CREATE", "HEARTBEAT"], []),
627 ["CHANNEL_CREATE", "HEARTBEAT", "CUSTOM"]
628 );
629 }
630
631 #[test]
632 fn order_event_tokens_without_custom_is_identity() {
633 assert_eq!(
634 order_event_tokens(["CHANNEL_CREATE", "HEARTBEAT"], []),
635 ["CHANNEL_CREATE", "HEARTBEAT"]
636 );
637 }
638
639 #[test]
640 fn order_event_tokens_introduces_custom_for_subclasses() {
641 assert_eq!(
642 order_event_tokens(["HEARTBEAT"], ["sofia::register"]),
643 ["HEARTBEAT", "CUSTOM", "sofia::register"]
644 );
645 }
646
647 #[test]
648 fn order_event_tokens_keeps_bare_custom_without_subclasses() {
649 assert_eq!(order_event_tokens(["CUSTOM"], []), ["CUSTOM"]);
650 }
651
652 #[test]
653 fn order_event_tokens_empty_is_empty() {
654 assert!(order_event_tokens([], []).is_empty());
655 }
656
657 #[test]
658 fn swallowed_event_types_reports_a_type_after_custom() {
659 assert_eq!(
660 swallowed_event_types("HEARTBEAT CUSTOM sofia::register CHANNEL_CREATE"),
661 ["CHANNEL_CREATE"]
662 );
663 }
664
665 #[test]
666 fn swallowed_event_types_empty_when_well_ordered() {
667 assert!(swallowed_event_types("HEARTBEAT CUSTOM sofia::register").is_empty());
668 }
669
670 #[test]
671 fn swallowed_event_types_empty_without_custom() {
672 assert!(swallowed_event_types("HEARTBEAT CHANNEL_CREATE").is_empty());
673 }
674
675 #[test]
676 fn swallowed_event_types_matches_custom_case_insensitively() {
677 assert_eq!(swallowed_event_types("custom HEARTBEAT"), ["HEARTBEAT"]);
678 }
679
680 #[test]
681 fn custom_first_serialises_last() {
682 let sub = EventSubscription::new(EventFormat::Plain)
683 .event(EslEventType::Custom)
684 .event(EslEventType::ChannelCreate)
685 .event(EslEventType::ChannelAnswer);
686 assert_eq!(
687 sub.to_event_string(),
688 Some("CHANNEL_CREATE CHANNEL_ANSWER CUSTOM".to_string())
689 );
690 }
691
692 #[test]
693 fn custom_in_middle_serialises_last() {
694 let sub = EventSubscription::new(EventFormat::Plain)
695 .event(EslEventType::Heartbeat)
696 .event(EslEventType::Custom)
697 .event(EslEventType::ChannelCreate);
698 assert_eq!(
699 sub.to_event_string(),
700 Some("HEARTBEAT CHANNEL_CREATE CUSTOM".to_string())
701 );
702 }
703
704 #[test]
705 fn custom_and_subclasses_follow_every_event_type() {
706 let sub = EventSubscription::new(EventFormat::Plain)
707 .event(EslEventType::Heartbeat)
708 .event(EslEventType::BackgroundJob)
709 .event(EslEventType::Custom)
710 .event(EslEventType::Unpublish)
711 .event(EslEventType::ChannelCreate)
712 .custom_subclass("sofia::gateway_state")
713 .unwrap();
714 assert_eq!(
715 sub.to_event_string(),
716 Some(
717 "HEARTBEAT BACKGROUND_JOB UNPUBLISH CHANNEL_CREATE CUSTOM sofia::gateway_state"
718 .to_string()
719 )
720 );
721 }
722
723 #[test]
724 fn raw_events_order_before_custom() {
725 let sub = EventSubscription::new(EventFormat::Plain)
726 .event(EslEventType::Custom)
727 .event_raw("FUTURE_EVENT")
728 .unwrap()
729 .custom_subclass("sofia::register")
730 .unwrap();
731 assert_eq!(
732 sub.to_event_string(),
733 Some("FUTURE_EVENT CUSTOM sofia::register".to_string())
734 );
735 }
736
737 #[test]
740 fn raw_custom_token_is_the_terminal_marker() {
741 let sub = EventSubscription::new(EventFormat::Plain)
742 .event(EslEventType::Custom)
743 .event_raw("custom")
744 .unwrap()
745 .event_raw("CHANNEL_CREATE")
746 .unwrap();
747 assert_eq!(
748 sub.to_event_string(),
749 Some("CHANNEL_CREATE CUSTOM".to_string())
750 );
751 }
752
753 #[test]
754 fn order_preserved_without_custom() {
755 let sub = EventSubscription::new(EventFormat::Plain)
756 .event(EslEventType::Heartbeat)
757 .event(EslEventType::ChannelAnswer)
758 .event(EslEventType::ChannelCreate)
759 .event(EslEventType::BackgroundJob);
760 assert_eq!(
761 sub.to_event_string(),
762 Some("HEARTBEAT CHANNEL_ANSWER CHANNEL_CREATE BACKGROUND_JOB".to_string())
763 );
764 }
765
766 #[test]
767 fn all_wins_from_any_position() {
768 let sub = EventSubscription::new(EventFormat::Plain)
769 .event(EslEventType::Custom)
770 .event(EslEventType::ChannelCreate)
771 .event(EslEventType::All)
772 .custom_subclass("sofia::register")
773 .unwrap();
774 assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
775 }
776
777 #[test]
782 fn no_requested_event_type_lands_after_custom() {
783 let subs = [
784 EventSubscription::new(EventFormat::Plain)
785 .event(EslEventType::Custom)
786 .event(EslEventType::ChannelCreate),
787 EventSubscription::new(EventFormat::Plain)
788 .event(EslEventType::Heartbeat)
789 .event(EslEventType::Custom)
790 .event(EslEventType::Unpublish)
791 .custom_subclass("sofia::register")
792 .unwrap(),
793 EventSubscription::new(EventFormat::Plain)
794 .events(EslEventType::CHANNEL_EVENTS)
795 .event(EslEventType::Custom)
796 .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS),
797 ];
798
799 for sub in subs {
800 let out = sub
801 .to_event_string()
802 .unwrap();
803 let tokens: Vec<&str> = out
804 .split(' ')
805 .collect();
806 let custom_at = tokens
807 .iter()
808 .position(|t| *t == "CUSTOM")
809 .expect("CUSTOM present");
810 for want in sub.event_types() {
811 if *want == EslEventType::Custom {
812 continue;
813 }
814 let at = tokens
815 .iter()
816 .position(|t| *t == want.as_str())
817 .unwrap_or_else(|| panic!("{} missing from {out:?}", want.as_str()));
818 assert!(
819 at < custom_at,
820 "{} lands after CUSTOM in {out:?}",
821 want.as_str()
822 );
823 }
824 }
825 }
826
827 #[test]
828 fn event_string_empty_is_none() {
829 let sub = EventSubscription::new(EventFormat::Plain);
830 assert_eq!(sub.to_event_string(), None);
831 }
832
833 #[test]
834 fn filters_preserve_order() {
835 let sub = EventSubscription::new(EventFormat::Plain)
836 .filter(EventHeader::CallDirection, "inbound")
837 .unwrap()
838 .filter_raw("X-Custom", "value1")
839 .unwrap()
840 .filter(EventHeader::ChannelState, "CS_EXECUTE")
841 .unwrap();
842 assert_eq!(
843 sub.filters(),
844 &[
845 ("Call-Direction".to_string(), "inbound".to_string()),
846 ("X-Custom".to_string(), "value1".to_string()),
847 ("Channel-State".to_string(), "CS_EXECUTE".to_string()),
848 ]
849 );
850 }
851
852 #[test]
853 fn builder_chain() {
854 let sub = EventSubscription::new(EventFormat::Plain)
855 .events(EslEventType::CHANNEL_EVENTS)
856 .event(EslEventType::Heartbeat)
857 .custom_subclass("sofia::register")
858 .unwrap()
859 .filter(EventHeader::CallDirection, "inbound")
860 .unwrap()
861 .with_format(EventFormat::Json);
862
863 assert_eq!(sub.format(), EventFormat::Json);
864 assert!(!sub.is_empty());
865 assert!(!sub.is_all());
866 assert!(sub
867 .event_types()
868 .contains(&EslEventType::ChannelCreate));
869 assert!(sub
870 .event_types()
871 .contains(&EslEventType::Heartbeat));
872 assert_eq!(sub.custom_subclass_list(), &["sofia::register"]);
873 assert_eq!(
874 sub.filters()
875 .len(),
876 1
877 );
878 }
879
880 #[test]
881 fn serde_round_trip_subscription() {
882 let sub = EventSubscription::new(EventFormat::Plain)
883 .event(EslEventType::ChannelCreate)
884 .event(EslEventType::Heartbeat)
885 .custom_subclass("sofia::register")
886 .unwrap()
887 .filter(EventHeader::CallDirection, "inbound")
888 .unwrap();
889
890 let json = serde_json::to_string(&sub).unwrap();
891 let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
892 assert_eq!(sub, deserialized);
893 }
894
895 #[test]
896 fn serde_rejects_invalid_subclass() {
897 let json =
898 r#"{"format":"Plain","events":[],"custom_subclasses":["bad subclass"],"filters":[]}"#;
899 let result: Result<EventSubscription, _> = serde_json::from_str(json);
900 assert!(result.is_err());
901 let err = result
902 .unwrap_err()
903 .to_string();
904 assert!(err.contains("space"), "error should mention space: {err}");
905 }
906
907 #[test]
908 fn serde_rejects_newline_in_filter() {
909 let json = r#"{"format":"Plain","events":[],"custom_subclasses":[],"filters":[["Header","val\n"]]}"#;
910 let result: Result<EventSubscription, _> = serde_json::from_str(json);
911 assert!(result.is_err());
912 let err = result
913 .unwrap_err()
914 .to_string();
915 assert!(
916 err.contains("newline"),
917 "error should mention newline: {err}"
918 );
919 }
920
921 #[test]
922 fn custom_subclass_rejects_space() {
923 let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad subclass");
924 assert!(result.is_err());
925 }
926
927 #[test]
928 fn custom_subclass_rejects_newline() {
929 let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad\nsubclass");
930 assert!(result.is_err());
931 }
932
933 #[test]
934 fn custom_subclass_rejects_empty() {
935 let result = EventSubscription::new(EventFormat::Plain).custom_subclass("");
936 assert!(result.is_err());
937 }
938
939 #[test]
940 fn filter_raw_rejects_newline_in_header() {
941 let result = EventSubscription::new(EventFormat::Plain).filter_raw("Bad\nHeader", "value");
942 assert!(result.is_err());
943 }
944
945 #[test]
946 fn filter_raw_rejects_newline_in_value() {
947 let result = EventSubscription::new(EventFormat::Plain).filter_raw("Header", "bad\nvalue");
948 assert!(result.is_err());
949 }
950
951 #[test]
952 fn filter_typed_rejects_newline_in_value() {
953 let result = EventSubscription::new(EventFormat::Plain)
954 .filter(EventHeader::CallDirection, "bad\nvalue");
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn sofia_event_single() {
960 let sub =
961 EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
962 assert_eq!(
963 sub.to_event_string(),
964 Some("CUSTOM sofia::register".to_string())
965 );
966 }
967
968 #[test]
969 fn sofia_events_group() {
970 let sub = EventSubscription::new(EventFormat::Plain)
971 .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
972 let event_str = sub
973 .to_event_string()
974 .unwrap();
975 assert!(event_str.starts_with("CUSTOM"));
976 assert!(event_str.contains("sofia::gateway_state"));
977 assert!(event_str.contains("sofia::gateway_add"));
978 assert!(event_str.contains("sofia::gateway_delete"));
979 assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
980 }
981
982 #[test]
983 fn event_raw_wire_string() {
984 let sub = EventSubscription::new(EventFormat::Plain)
985 .event(EslEventType::Heartbeat)
986 .event_raw("NEW_EVENT_NOT_IN_ENUM")
987 .unwrap();
988 assert_eq!(
989 sub.to_event_string(),
990 Some("HEARTBEAT NEW_EVENT_NOT_IN_ENUM".to_string())
991 );
992 }
993
994 #[test]
995 fn events_raw_wire_string() {
996 let sub = EventSubscription::new(EventFormat::Plain)
997 .events_raw(["FUTURE_A", "FUTURE_B"])
998 .unwrap();
999 assert_eq!(sub.to_event_string(), Some("FUTURE_A FUTURE_B".to_string()));
1000 }
1001
1002 #[test]
1003 fn event_raw_with_custom_subclass() {
1004 let sub = EventSubscription::new(EventFormat::Plain)
1005 .event_raw("NEW_EVENT")
1006 .unwrap()
1007 .custom_subclass("sofia::register")
1008 .unwrap();
1009 assert_eq!(
1010 sub.to_event_string(),
1011 Some("NEW_EVENT CUSTOM sofia::register".to_string())
1012 );
1013 }
1014
1015 #[test]
1016 fn event_raw_rejects_newline() {
1017 assert!(EventSubscription::new(EventFormat::Plain)
1018 .event_raw("bad\nevent")
1019 .is_err());
1020 }
1021
1022 #[test]
1023 fn event_raw_rejects_space() {
1024 assert!(EventSubscription::new(EventFormat::Plain)
1025 .event_raw("bad event")
1026 .is_err());
1027 }
1028
1029 #[test]
1030 fn event_raw_rejects_empty() {
1031 assert!(EventSubscription::new(EventFormat::Plain)
1032 .event_raw("")
1033 .is_err());
1034 }
1035
1036 #[test]
1037 fn events_raw_errors_on_first_invalid() {
1038 let result =
1039 EventSubscription::new(EventFormat::Plain).events_raw(["GOOD", "bad event", "OTHER"]);
1040 assert!(result.is_err());
1041 }
1042
1043 #[test]
1044 fn event_types_raw_mut_mutable() {
1045 let mut sub = EventSubscription::new(EventFormat::Plain);
1046 sub.event_types_raw_mut()
1047 .push("DIRECT_PUSH".to_string());
1048 assert_eq!(sub.event_types_raw(), &["DIRECT_PUSH".to_string()]);
1049 }
1050
1051 #[test]
1052 fn is_empty_sees_raw_events() {
1053 let sub = EventSubscription::new(EventFormat::Plain)
1054 .event_raw("ONLY_RAW")
1055 .unwrap();
1056 assert!(!sub.is_empty());
1057 }
1058
1059 #[test]
1060 fn serde_round_trip_with_raw_events() {
1061 let sub = EventSubscription::new(EventFormat::Plain)
1062 .event(EslEventType::ChannelCreate)
1063 .event_raw("FUTURE_EVENT")
1064 .unwrap()
1065 .custom_subclass("sofia::register")
1066 .unwrap();
1067
1068 let json = serde_json::to_string(&sub).unwrap();
1069 let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
1070 assert_eq!(sub, deserialized);
1071 }
1072
1073 #[test]
1074 fn serde_rejects_invalid_raw_event() {
1075 let json = r#"{"format":"Plain","events":[],"raw_events":["bad event"],"custom_subclasses":[],"filters":[]}"#;
1076 let result: Result<EventSubscription, _> = serde_json::from_str(json);
1077 assert!(result.is_err());
1078 }
1079
1080 #[test]
1081 fn serde_missing_raw_events_field_defaults_to_empty() {
1082 let json =
1085 r#"{"format":"Plain","events":["Heartbeat"],"custom_subclasses":[],"filters":[]}"#;
1086 let sub: EventSubscription = serde_json::from_str(json).unwrap();
1087 assert!(sub
1088 .event_types_raw()
1089 .is_empty());
1090 }
1091
1092 #[test]
1093 fn sofia_event_mixed_with_typed_events() {
1094 let sub = EventSubscription::new(EventFormat::Plain)
1095 .event(EslEventType::Heartbeat)
1096 .sofia_event(SofiaEventSubclass::GatewayState);
1097 assert_eq!(
1098 sub.to_event_string(),
1099 Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
1100 );
1101 }
1102}