Skip to main content

freeswitch_types/event/
subscription.rs

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/// Error returned when an [`EventSubscription`] builder method receives invalid input.
29///
30/// Custom subclasses and filter values are validated against ESL wire-safety
31/// constraints: no newlines, carriage returns, or (for subclasses) spaces.
32/// `Display` names the character class that made the value unusable and the
33/// value's size; the value itself is subscriber data and stays on the field.
34#[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/// Declarative description of an ESL event subscription.
57///
58/// Captures the event format, event types, custom subclasses, and filters
59/// as a single unit. Useful for config-driven subscriptions and reconnection
60/// patterns where the caller needs to rebuild subscriptions from a saved
61/// description.
62///
63/// # Wire safety
64///
65/// Builder methods validate inputs against ESL wire injection risks.
66/// Custom subclasses reject `\n`, `\r`, spaces, and empty strings.
67/// Filter headers and values reject `\n` and `\r`.
68///
69/// # Example
70///
71/// ```rust
72/// use freeswitch_types::{EventSubscription, EventFormat, EslEventType, EventHeader};
73///
74/// let sub = EventSubscription::new(EventFormat::Plain)
75///     .events(EslEventType::CHANNEL_EVENTS)
76///     .event(EslEventType::Heartbeat)
77///     .custom_subclass("sofia::register").unwrap()
78///     .filter(EventHeader::CallDirection, "inbound").unwrap();
79///
80/// assert!(!sub.is_empty());
81/// assert!(!sub.is_all());
82/// ```
83// qual:allow(srp, god_struct) reason: "public builder; accessors read disjoint fields"
84#[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/// What a field accepts beyond the newline ban every wire value carries.
94#[derive(Debug, Clone, Copy)]
95struct WireTokenRules {
96    reject_empty: bool,
97    reject_space: bool,
98}
99
100impl WireTokenRules {
101    /// One wire token: the switch splits on spaces and drops an empty one.
102    const TOKEN: Self = Self {
103        reject_empty: true,
104        reject_space: true,
105    };
106
107    /// Free text within a header line, where only the terminators are unusable.
108    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::Newline) => 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
145/// Order event names and custom subclasses for the ESL `event` / `nixevent`
146/// grammar.
147///
148/// `CUSTOM` is terminal: FreeSWITCH reads every token after it as a subclass
149/// name rather than as an event type. A `CUSTOM` appearing anywhere in `names`
150/// is moved to the tail, deduplicated, and emitted in the canonical spelling
151/// (event names match case-insensitively on the wire); one is introduced when
152/// `subclasses` is non-empty and `names` did not carry it. The relative order
153/// of every other name is preserved.
154///
155/// Pass an empty `subclasses` for a `nixevent` list.
156pub 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
186/// Event-type names in a raw `event` / `nixevent` token list that FreeSWITCH
187/// will register as `CUSTOM` subclass names instead of subscribing.
188///
189/// Empty when the list is well ordered or carries no `CUSTOM`. A subclass
190/// genuinely named like an event type is indistinguishable from the mistake in
191/// a flat token list, so this reports rather than rejects — it exists for the
192/// raw string commands, which cannot tell the two apart.
193/// [`EventSubscription`] keeps them in separate fields and cannot produce the
194/// mistake at all.
195///
196/// ```rust
197/// use freeswitch_types::event::swallowed_event_types;
198///
199/// assert!(swallowed_event_types("HEARTBEAT CUSTOM sofia::register").is_empty());
200/// assert_eq!(
201///     swallowed_event_types("CUSTOM sofia::register CHANNEL_CREATE"),
202///     ["CHANNEL_CREATE"]
203/// );
204/// ```
205pub 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    /// Create an empty subscription with the given format.
219    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    /// Create a subscription for all events.
230    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    /// Add a single event type.
241    pub fn event(mut self, event: EslEventType) -> Self {
242        self.events
243            .push(event);
244        self
245    }
246
247    /// Add multiple event types (e.g. from group constants like `EslEventType::CHANNEL_EVENTS`).
248    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    /// Add a single event by wire name.
262    ///
263    /// Escape hatch for events the [`EslEventType`] enum hasn't yet been
264    /// updated to cover. The argument is validated for newline injection,
265    /// spaces, and emptiness.
266    ///
267    /// Raw events appear on the wire alongside typed events when
268    /// [`to_event_string()`](Self::to_event_string) is called.
269    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    /// Add multiple events by wire name.
278    ///
279    /// Returns `Err` on the first invalid entry.
280    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    /// Add a custom subclass (e.g. `"sofia::register"`).
295    ///
296    /// Returns `Err` if the subclass contains spaces, newlines, or is empty.
297    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    /// Add multiple custom subclasses.
309    ///
310    /// Returns `Err` on the first invalid subclass.
311    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    /// Subscribe to a single Sofia event subclass.
325    ///
326    /// Convenience wrapper around [`custom_subclass()`](Self::custom_subclass) that
327    /// accepts a typed [`SofiaEventSubclass`] instead of a raw string.
328    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    /// Subscribe to multiple Sofia event subclasses.
339    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    /// Add a filter with a typed header.
357    ///
358    /// The header enum is always valid; only the value is validated.
359    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    /// Add a filter with raw header and value strings.
378    ///
379    /// Both header and value are validated against newline injection.
380    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    /// Change the event format.
396    pub fn with_format(mut self, format: EventFormat) -> Self {
397        self.format = format;
398        self
399    }
400
401    /// The event format.
402    pub fn format(&self) -> EventFormat {
403        self.format
404    }
405
406    /// Mutable reference to the event format.
407    pub fn format_mut(&mut self) -> &mut EventFormat {
408        &mut self.format
409    }
410
411    /// The subscribed event types.
412    pub fn event_types(&self) -> &[EslEventType] {
413        &self.events
414    }
415
416    /// Mutable access to the event types list.
417    pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
418        &mut self.events
419    }
420
421    /// Events subscribed by raw wire name (see [`event_raw`](Self::event_raw)).
422    pub fn event_types_raw(&self) -> &[String] {
423        &self.raw_events
424    }
425
426    /// Mutable access to the raw event list.
427    ///
428    /// Direct push to this list bypasses [`event_raw`](Self::event_raw)'s
429    /// validation. Callers are responsible for ensuring entries contain no
430    /// newlines, spaces, or empty strings.
431    pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
432        &mut self.raw_events
433    }
434
435    /// The subscribed custom subclasses.
436    pub fn custom_subclass_list(&self) -> &[String] {
437        &self.custom_subclasses
438    }
439
440    /// Mutable access to the custom subclasses list.
441    pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
442        &mut self.custom_subclasses
443    }
444
445    /// The event filters as (header, value) pairs.
446    pub fn filters(&self) -> &[(String, String)] {
447        &self.filters
448    }
449
450    /// Mutable access to the filters list.
451    pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
452        &mut self.filters
453    }
454
455    /// Whether the subscription includes all events.
456    pub fn is_all(&self) -> bool {
457        self.events
458            .contains(&EslEventType::All)
459    }
460
461    /// Whether the subscription has no events, no raw events, and no
462    /// custom subclasses.
463    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    /// Build the event string for the ESL `event` command.
475    ///
476    /// Returns `None` if no events, raw events, or custom subclasses are
477    /// configured. Returns `Some("ALL")` if `EslEventType::All` is present.
478    ///
479    /// Otherwise: typed event names then raw event names in the order they were
480    /// added, followed by `CUSTOM` and the custom subclasses. The order the
481    /// caller added event types in is not load-bearing — see
482    /// [`order_event_tokens`].
483    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        // Should not have "CUSTOM" twice
656        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    /// A raw `CUSTOM` is the terminal marker, not a plain name. FreeSWITCH
777    /// matches event names case-insensitively, so canonicalising is lossless.
778    #[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    /// The property FreeSWITCH's grammar requires, asserted against
817    /// `event_types()` rather than against the emitted tokens: a subclass may
818    /// legitimately be named like an event type, and the flat string cannot
819    /// tell the two apart.
820    #[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("newline"),
956            "error should mention newline: {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 newline (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_typed_rejects_newline_in_value() {
1018        let result = EventSubscription::new(EventFormat::Plain)
1019            .filter(EventHeader::CallDirection, "bad\nvalue");
1020        assert!(result.is_err());
1021    }
1022
1023    #[test]
1024    fn sofia_event_single() {
1025        let sub =
1026            EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
1027        assert_eq!(
1028            sub.to_event_string(),
1029            Some("CUSTOM sofia::register".to_string())
1030        );
1031    }
1032
1033    #[test]
1034    fn sofia_events_group() {
1035        let sub = EventSubscription::new(EventFormat::Plain)
1036            .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
1037        let event_str = sub
1038            .to_event_string()
1039            .unwrap();
1040        assert!(event_str.starts_with("CUSTOM"));
1041        assert!(event_str.contains("sofia::gateway_state"));
1042        assert!(event_str.contains("sofia::gateway_add"));
1043        assert!(event_str.contains("sofia::gateway_delete"));
1044        assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
1045    }
1046
1047    #[test]
1048    fn event_raw_wire_string() {
1049        let sub = EventSubscription::new(EventFormat::Plain)
1050            .event(EslEventType::Heartbeat)
1051            .event_raw("NEW_EVENT_NOT_IN_ENUM")
1052            .unwrap();
1053        assert_eq!(
1054            sub.to_event_string(),
1055            Some("HEARTBEAT NEW_EVENT_NOT_IN_ENUM".to_string())
1056        );
1057    }
1058
1059    #[test]
1060    fn events_raw_wire_string() {
1061        let sub = EventSubscription::new(EventFormat::Plain)
1062            .events_raw(["FUTURE_A", "FUTURE_B"])
1063            .unwrap();
1064        assert_eq!(sub.to_event_string(), Some("FUTURE_A FUTURE_B".to_string()));
1065    }
1066
1067    #[test]
1068    fn event_raw_with_custom_subclass() {
1069        let sub = EventSubscription::new(EventFormat::Plain)
1070            .event_raw("NEW_EVENT")
1071            .unwrap()
1072            .custom_subclass("sofia::register")
1073            .unwrap();
1074        assert_eq!(
1075            sub.to_event_string(),
1076            Some("NEW_EVENT CUSTOM sofia::register".to_string())
1077        );
1078    }
1079
1080    #[test]
1081    fn event_raw_rejects_newline() {
1082        assert!(EventSubscription::new(EventFormat::Plain)
1083            .event_raw("bad\nevent")
1084            .is_err());
1085    }
1086
1087    #[test]
1088    fn event_raw_rejects_space() {
1089        assert!(EventSubscription::new(EventFormat::Plain)
1090            .event_raw("bad event")
1091            .is_err());
1092    }
1093
1094    #[test]
1095    fn event_raw_rejects_empty() {
1096        assert!(EventSubscription::new(EventFormat::Plain)
1097            .event_raw("")
1098            .is_err());
1099    }
1100
1101    #[test]
1102    fn events_raw_errors_on_first_invalid() {
1103        let result =
1104            EventSubscription::new(EventFormat::Plain).events_raw(["GOOD", "bad event", "OTHER"]);
1105        assert!(result.is_err());
1106    }
1107
1108    #[test]
1109    fn event_types_raw_mut_mutable() {
1110        let mut sub = EventSubscription::new(EventFormat::Plain);
1111        sub.event_types_raw_mut()
1112            .push("DIRECT_PUSH".to_string());
1113        assert_eq!(sub.event_types_raw(), &["DIRECT_PUSH".to_string()]);
1114    }
1115
1116    #[test]
1117    fn is_empty_sees_raw_events() {
1118        let sub = EventSubscription::new(EventFormat::Plain)
1119            .event_raw("ONLY_RAW")
1120            .unwrap();
1121        assert!(!sub.is_empty());
1122    }
1123
1124    #[test]
1125    fn serde_round_trip_with_raw_events() {
1126        let sub = EventSubscription::new(EventFormat::Plain)
1127            .event(EslEventType::ChannelCreate)
1128            .event_raw("FUTURE_EVENT")
1129            .unwrap()
1130            .custom_subclass("sofia::register")
1131            .unwrap();
1132
1133        let json = serde_json::to_string(&sub).unwrap();
1134        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
1135        assert_eq!(sub, deserialized);
1136    }
1137
1138    #[test]
1139    fn serde_rejects_invalid_raw_event() {
1140        let json = r#"{"format":"Plain","events":[],"raw_events":["bad event"],"custom_subclasses":[],"filters":[]}"#;
1141        let result: Result<EventSubscription, _> = serde_json::from_str(json);
1142        assert!(result.is_err());
1143    }
1144
1145    #[test]
1146    fn serde_missing_raw_events_field_defaults_to_empty() {
1147        // Back-compat: configs written before raw_events was added must still
1148        // deserialize.
1149        let json =
1150            r#"{"format":"Plain","events":["Heartbeat"],"custom_subclasses":[],"filters":[]}"#;
1151        let sub: EventSubscription = serde_json::from_str(json).unwrap();
1152        assert!(sub
1153            .event_types_raw()
1154            .is_empty());
1155    }
1156
1157    #[test]
1158    fn sofia_event_mixed_with_typed_events() {
1159        let sub = EventSubscription::new(EventFormat::Plain)
1160            .event(EslEventType::Heartbeat)
1161            .sofia_event(SofiaEventSubclass::GatewayState);
1162        assert_eq!(
1163            sub.to_event_string(),
1164            Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
1165        );
1166    }
1167}