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#[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/// What a field accepts beyond the newline ban every wire value carries.
93#[derive(Debug, Clone, Copy)]
94struct WireTokenRules {
95    reject_empty: bool,
96    reject_space: bool,
97}
98
99impl WireTokenRules {
100    /// One wire token: the switch splits on spaces and drops an empty one.
101    const TOKEN: Self = Self {
102        reject_empty: true,
103        reject_space: true,
104    };
105
106    /// Free text within a header line, where only the terminators are unusable.
107    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
144/// Order event names and custom subclasses for the ESL `event` / `nixevent`
145/// grammar.
146///
147/// `CUSTOM` is terminal: FreeSWITCH reads every token after it as a subclass
148/// name rather than as an event type. A `CUSTOM` appearing anywhere in `names`
149/// is moved to the tail, deduplicated, and emitted in the canonical spelling
150/// (event names match case-insensitively on the wire); one is introduced when
151/// `subclasses` is non-empty and `names` did not carry it. The relative order
152/// of every other name is preserved.
153///
154/// Pass an empty `subclasses` for a `nixevent` list.
155pub 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
185/// Event-type names in a raw `event` / `nixevent` token list that FreeSWITCH
186/// will register as `CUSTOM` subclass names instead of subscribing.
187///
188/// Empty when the list is well ordered or carries no `CUSTOM`. A subclass
189/// genuinely named like an event type is indistinguishable from the mistake in
190/// a flat token list, so this reports rather than rejects — it exists for the
191/// raw string commands, which cannot tell the two apart.
192/// [`EventSubscription`] keeps them in separate fields and cannot produce the
193/// mistake at all.
194///
195/// ```rust
196/// use freeswitch_types::event::swallowed_event_types;
197///
198/// assert!(swallowed_event_types("HEARTBEAT CUSTOM sofia::register").is_empty());
199/// assert_eq!(
200///     swallowed_event_types("CUSTOM sofia::register CHANNEL_CREATE"),
201///     ["CHANNEL_CREATE"]
202/// );
203/// ```
204pub 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    /// Create an empty subscription with the given format.
218    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    /// Create a subscription for all events.
229    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    /// Add a single event type.
240    pub fn event(mut self, event: EslEventType) -> Self {
241        self.events
242            .push(event);
243        self
244    }
245
246    /// Add multiple event types (e.g. from group constants like `EslEventType::CHANNEL_EVENTS`).
247    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    /// Add a single event by wire name.
261    ///
262    /// Escape hatch for events the [`EslEventType`] enum hasn't yet been
263    /// updated to cover. The argument is validated for newline injection,
264    /// spaces, and emptiness.
265    ///
266    /// Raw events appear on the wire alongside typed events when
267    /// [`to_event_string()`](Self::to_event_string) is called.
268    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    /// Add multiple events by wire name.
277    ///
278    /// Returns `Err` on the first invalid entry.
279    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    /// Add a custom subclass (e.g. `"sofia::register"`).
294    ///
295    /// Returns `Err` if the subclass contains spaces, newlines, or is empty.
296    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    /// Add multiple custom subclasses.
308    ///
309    /// Returns `Err` on the first invalid subclass.
310    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    /// Subscribe to a single Sofia event subclass.
324    ///
325    /// Convenience wrapper around [`custom_subclass()`](Self::custom_subclass) that
326    /// accepts a typed [`SofiaEventSubclass`] instead of a raw string.
327    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    /// Subscribe to multiple Sofia event subclasses.
338    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    /// Add a filter with a typed header.
356    ///
357    /// The header enum is always valid; only the value is validated.
358    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    /// Add a filter with raw header and value strings.
377    ///
378    /// Both header and value are validated against newline injection.
379    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    /// Change the event format.
395    pub fn with_format(mut self, format: EventFormat) -> Self {
396        self.format = format;
397        self
398    }
399
400    /// The event format.
401    pub fn format(&self) -> EventFormat {
402        self.format
403    }
404
405    /// Mutable reference to the event format.
406    pub fn format_mut(&mut self) -> &mut EventFormat {
407        &mut self.format
408    }
409
410    /// The subscribed event types.
411    pub fn event_types(&self) -> &[EslEventType] {
412        &self.events
413    }
414
415    /// Mutable access to the event types list.
416    pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
417        &mut self.events
418    }
419
420    /// Events subscribed by raw wire name (see [`event_raw`](Self::event_raw)).
421    pub fn event_types_raw(&self) -> &[String] {
422        &self.raw_events
423    }
424
425    /// Mutable access to the raw event list.
426    ///
427    /// Direct push to this list bypasses [`event_raw`](Self::event_raw)'s
428    /// validation. Callers are responsible for ensuring entries contain no
429    /// newlines, spaces, or empty strings.
430    pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
431        &mut self.raw_events
432    }
433
434    /// The subscribed custom subclasses.
435    pub fn custom_subclass_list(&self) -> &[String] {
436        &self.custom_subclasses
437    }
438
439    /// Mutable access to the custom subclasses list.
440    pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
441        &mut self.custom_subclasses
442    }
443
444    /// The event filters as (header, value) pairs.
445    pub fn filters(&self) -> &[(String, String)] {
446        &self.filters
447    }
448
449    /// Mutable access to the filters list.
450    pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
451        &mut self.filters
452    }
453
454    /// Whether the subscription includes all events.
455    pub fn is_all(&self) -> bool {
456        self.events
457            .contains(&EslEventType::All)
458    }
459
460    /// Whether the subscription has no events, no raw events, and no
461    /// custom subclasses.
462    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    /// Build the event string for the ESL `event` command.
474    ///
475    /// Returns `None` if no events, raw events, or custom subclasses are
476    /// configured. Returns `Some("ALL")` if `EslEventType::All` is present.
477    ///
478    /// Otherwise: typed event names then raw event names in the order they were
479    /// added, followed by `CUSTOM` and the custom subclasses. The order the
480    /// caller added event types in is not load-bearing — see
481    /// [`order_event_tokens`].
482    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        // Should not have "CUSTOM" twice
655        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    /// A raw `CUSTOM` is the terminal marker, not a plain name. FreeSWITCH
776    /// matches event names case-insensitively, so canonicalising is lossless.
777    #[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    /// The property FreeSWITCH's grammar requires, asserted against
816    /// `event_types()` rather than against the emitted tokens: a subclass may
817    /// legitimately be named like an event type, and the flat string cannot
818    /// tell the two apart.
819    #[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        // Back-compat: configs written before raw_events was added must still
1147        // deserialize.
1148        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}