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/// Error returned when an [`EventSubscription`] builder method receives invalid input.
8///
9/// Custom subclasses and filter values are validated against ESL wire-safety
10/// constraints: no newlines, carriage returns, or (for subclasses) spaces.
11#[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/// Declarative description of an ESL event subscription.
23///
24/// Captures the event format, event types, custom subclasses, and filters
25/// as a single unit. Useful for config-driven subscriptions and reconnection
26/// patterns where the caller needs to rebuild subscriptions from a saved
27/// description.
28///
29/// # Wire safety
30///
31/// Builder methods validate inputs against ESL wire injection risks.
32/// Custom subclasses reject `\n`, `\r`, spaces, and empty strings.
33/// Filter headers and values reject `\n` and `\r`.
34///
35/// # Example
36///
37/// ```rust
38/// use freeswitch_types::{EventSubscription, EventFormat, EslEventType, EventHeader};
39///
40/// let sub = EventSubscription::new(EventFormat::Plain)
41///     .events(EslEventType::CHANNEL_EVENTS)
42///     .event(EslEventType::Heartbeat)
43///     .custom_subclass("sofia::register").unwrap()
44///     .filter(EventHeader::CallDirection, "inbound").unwrap();
45///
46/// assert!(!sub.is_empty());
47/// assert!(!sub.is_all());
48/// ```
49#[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
59/// Wire-safety validator shared by raw events, custom subclasses, and filter fields.
60///
61/// Newlines/CRs are always rejected (would inject extra ESL commands). When
62/// `reject_empty` is set the value must be non-empty. When `reject_space` is
63/// set spaces are rejected (token splitting on the wire).
64fn 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
106/// Order event names and custom subclasses for the ESL `event` / `nixevent`
107/// grammar.
108///
109/// `CUSTOM` is terminal: FreeSWITCH reads every token after it as a subclass
110/// name rather than as an event type. A `CUSTOM` appearing anywhere in `names`
111/// is moved to the tail, deduplicated, and emitted in the canonical spelling
112/// (event names match case-insensitively on the wire); one is introduced when
113/// `subclasses` is non-empty and `names` did not carry it. The relative order
114/// of every other name is preserved.
115///
116/// Pass an empty `subclasses` for a `nixevent` list.
117pub 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
147/// Event-type names in a raw `event` / `nixevent` token list that FreeSWITCH
148/// will register as `CUSTOM` subclass names instead of subscribing.
149///
150/// Empty when the list is well ordered or carries no `CUSTOM`. A subclass
151/// genuinely named like an event type is indistinguishable from the mistake in
152/// a flat token list, so this reports rather than rejects — it exists for the
153/// raw string commands, which cannot tell the two apart.
154/// [`EventSubscription`] keeps them in separate fields and cannot produce the
155/// mistake at all.
156///
157/// ```rust
158/// use freeswitch_types::event::swallowed_event_types;
159///
160/// assert!(swallowed_event_types("HEARTBEAT CUSTOM sofia::register").is_empty());
161/// assert_eq!(
162///     swallowed_event_types("CUSTOM sofia::register CHANNEL_CREATE"),
163///     ["CHANNEL_CREATE"]
164/// );
165/// ```
166pub 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    /// Create an empty subscription with the given format.
180    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    /// Create a subscription for all events.
191    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    /// Add a single event type.
202    pub fn event(mut self, event: EslEventType) -> Self {
203        self.events
204            .push(event);
205        self
206    }
207
208    /// Add multiple event types (e.g. from group constants like `EslEventType::CHANNEL_EVENTS`).
209    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    /// Add a single event by wire name.
223    ///
224    /// Escape hatch for events the [`EslEventType`] enum hasn't yet been
225    /// updated to cover. The argument is validated for newline injection,
226    /// spaces, and emptiness.
227    ///
228    /// Raw events appear on the wire alongside typed events when
229    /// [`to_event_string()`](Self::to_event_string) is called.
230    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    /// Add multiple events by wire name.
239    ///
240    /// Returns `Err` on the first invalid entry.
241    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    /// Add a custom subclass (e.g. `"sofia::register"`).
256    ///
257    /// Returns `Err` if the subclass contains spaces, newlines, or is empty.
258    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    /// Add multiple custom subclasses.
270    ///
271    /// Returns `Err` on the first invalid subclass.
272    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    /// Subscribe to a single Sofia event subclass.
286    ///
287    /// Convenience wrapper around [`custom_subclass()`](Self::custom_subclass) that
288    /// accepts a typed [`SofiaEventSubclass`] instead of a raw string.
289    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    /// Subscribe to multiple Sofia event subclasses.
300    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    /// Add a filter with a typed header.
318    ///
319    /// The header enum is always valid; only the value is validated.
320    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    /// Add a filter with raw header and value strings.
339    ///
340    /// Both header and value are validated against newline injection.
341    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    /// Change the event format.
357    pub fn with_format(mut self, format: EventFormat) -> Self {
358        self.format = format;
359        self
360    }
361
362    /// The event format.
363    pub fn format(&self) -> EventFormat {
364        self.format
365    }
366
367    /// Mutable reference to the event format.
368    pub fn format_mut(&mut self) -> &mut EventFormat {
369        &mut self.format
370    }
371
372    /// The subscribed event types.
373    pub fn event_types(&self) -> &[EslEventType] {
374        &self.events
375    }
376
377    /// Mutable access to the event types list.
378    pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
379        &mut self.events
380    }
381
382    /// Events subscribed by raw wire name (see [`event_raw`](Self::event_raw)).
383    pub fn event_types_raw(&self) -> &[String] {
384        &self.raw_events
385    }
386
387    /// Mutable access to the raw event list.
388    ///
389    /// Direct push to this list bypasses [`event_raw`](Self::event_raw)'s
390    /// validation. Callers are responsible for ensuring entries contain no
391    /// newlines, spaces, or empty strings.
392    pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
393        &mut self.raw_events
394    }
395
396    /// The subscribed custom subclasses.
397    pub fn custom_subclass_list(&self) -> &[String] {
398        &self.custom_subclasses
399    }
400
401    /// Mutable access to the custom subclasses list.
402    pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
403        &mut self.custom_subclasses
404    }
405
406    /// The event filters as (header, value) pairs.
407    pub fn filters(&self) -> &[(String, String)] {
408        &self.filters
409    }
410
411    /// Mutable access to the filters list.
412    pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
413        &mut self.filters
414    }
415
416    /// Whether the subscription includes all events.
417    pub fn is_all(&self) -> bool {
418        self.events
419            .contains(&EslEventType::All)
420    }
421
422    /// Whether the subscription has no events, no raw events, and no
423    /// custom subclasses.
424    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    /// Build the event string for the ESL `event` command.
436    ///
437    /// Returns `None` if no events, raw events, or custom subclasses are
438    /// configured. Returns `Some("ALL")` if `EslEventType::All` is present.
439    ///
440    /// Otherwise: typed event names then raw event names in the order they were
441    /// added, followed by `CUSTOM` and the custom subclasses. The order the
442    /// caller added event types in is not load-bearing — see
443    /// [`order_event_tokens`].
444    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        // Should not have "CUSTOM" twice
617        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    /// A raw `CUSTOM` is the terminal marker, not a plain name. FreeSWITCH
738    /// matches event names case-insensitively, so canonicalising is lossless.
739    #[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    /// The property FreeSWITCH's grammar requires, asserted against
778    /// `event_types()` rather than against the emitted tokens: a subclass may
779    /// legitimately be named like an event type, and the flat string cannot
780    /// tell the two apart.
781    #[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        // Back-compat: configs written before raw_events was added must still
1083        // deserialize.
1084        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}