Skip to main content

freeswitch_types/
event.rs

1//! ESL event types and structures
2
3use crate::headers::{normalize_header_key, EventHeader};
4use crate::lookup::HeaderLookup;
5use crate::sofia::SofiaEventSubclass;
6use crate::variables::{EslArray, EslArrayError};
7use indexmap::IndexMap;
8use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
9use std::fmt;
10use std::str::FromStr;
11
12/// Event format types supported by FreeSWITCH ESL
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[non_exhaustive]
16pub enum EventFormat {
17    /// Plain text format (default)
18    Plain,
19    /// JSON format
20    Json,
21    /// XML format
22    Xml,
23}
24
25impl EventFormat {
26    /// Determine event format from a Content-Type header value.
27    ///
28    /// Returns `Err` for unrecognized content types to avoid silently
29    /// misparsing events if FreeSWITCH adds a new format.
30    pub fn from_content_type(ct: &str) -> Result<Self, ParseEventFormatError> {
31        match ct {
32            "text/event-json" => Ok(Self::Json),
33            "text/event-xml" => Ok(Self::Xml),
34            "text/event-plain" => Ok(Self::Plain),
35            _ => Err(ParseEventFormatError(ct.to_string())),
36        }
37    }
38}
39
40impl fmt::Display for EventFormat {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            EventFormat::Plain => write!(f, "plain"),
44            EventFormat::Json => write!(f, "json"),
45            EventFormat::Xml => write!(f, "xml"),
46        }
47    }
48}
49
50impl FromStr for EventFormat {
51    type Err = ParseEventFormatError;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        if s.eq_ignore_ascii_case("plain") {
55            Ok(Self::Plain)
56        } else if s.eq_ignore_ascii_case("json") {
57            Ok(Self::Json)
58        } else if s.eq_ignore_ascii_case("xml") {
59            Ok(Self::Xml)
60        } else {
61            Err(ParseEventFormatError(s.to_string()))
62        }
63    }
64}
65
66/// Error returned when parsing an invalid event format string.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ParseEventFormatError(pub String);
69
70impl fmt::Display for ParseEventFormatError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "unknown event format: {}", self.0)
73    }
74}
75
76impl std::error::Error for ParseEventFormatError {}
77
78/// Generates `EslEventType` enum with `Display`, `FromStr`, `as_str`, and `parse_event_type`.
79macro_rules! esl_event_types {
80    (
81        $(
82            $(#[$attr:meta])*
83            $variant:ident => $wire:literal
84        ),+ $(,)?
85        ;
86        // Extra variants not in the main match (after All)
87        $(
88            $(#[$extra_attr:meta])*
89            $extra_variant:ident => $extra_wire:literal
90        ),* $(,)?
91    ) => {
92        /// FreeSWITCH event types matching the canonical order from `esl_event.h`
93        /// and `switch_event.c` EVENT_NAMES[].
94        ///
95        /// Variant names are the canonical wire names (e.g. `ChannelCreate` = `CHANNEL_CREATE`).
96        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98        #[non_exhaustive]
99        #[allow(missing_docs)]
100        pub enum EslEventType {
101            $(
102                $(#[$attr])*
103                $variant,
104            )+
105            $(
106                $(#[$extra_attr])*
107                $extra_variant,
108            )*
109        }
110
111        impl fmt::Display for EslEventType {
112            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113                f.write_str(self.as_str())
114            }
115        }
116
117        impl EslEventType {
118            /// Returns the canonical wire name as a static string slice.
119            pub const fn as_str(&self) -> &'static str {
120                match self {
121                    $( EslEventType::$variant => $wire, )+
122                    $( EslEventType::$extra_variant => $extra_wire, )*
123                }
124            }
125
126            /// Parse event type from wire name (canonical case).
127            pub fn parse_event_type(s: &str) -> Option<Self> {
128                match s {
129                    $( $wire => Some(EslEventType::$variant), )+
130                    $( $extra_wire => Some(EslEventType::$extra_variant), )*
131                    _ => None,
132                }
133            }
134        }
135
136        impl FromStr for EslEventType {
137            type Err = ParseEventTypeError;
138
139            fn from_str(s: &str) -> Result<Self, Self::Err> {
140                Self::parse_event_type(s).ok_or_else(|| ParseEventTypeError(s.to_string()))
141            }
142        }
143    };
144}
145
146esl_event_types! {
147    Custom => "CUSTOM",
148    Clone => "CLONE",
149    ChannelCreate => "CHANNEL_CREATE",
150    ChannelDestroy => "CHANNEL_DESTROY",
151    ChannelState => "CHANNEL_STATE",
152    ChannelCallstate => "CHANNEL_CALLSTATE",
153    ChannelAnswer => "CHANNEL_ANSWER",
154    ChannelHangup => "CHANNEL_HANGUP",
155    ChannelHangupComplete => "CHANNEL_HANGUP_COMPLETE",
156    ChannelExecute => "CHANNEL_EXECUTE",
157    ChannelExecuteComplete => "CHANNEL_EXECUTE_COMPLETE",
158    ChannelHold => "CHANNEL_HOLD",
159    ChannelUnhold => "CHANNEL_UNHOLD",
160    ChannelBridge => "CHANNEL_BRIDGE",
161    ChannelUnbridge => "CHANNEL_UNBRIDGE",
162    ChannelProgress => "CHANNEL_PROGRESS",
163    ChannelProgressMedia => "CHANNEL_PROGRESS_MEDIA",
164    ChannelOutgoing => "CHANNEL_OUTGOING",
165    ChannelPark => "CHANNEL_PARK",
166    ChannelUnpark => "CHANNEL_UNPARK",
167    ChannelApplication => "CHANNEL_APPLICATION",
168    ChannelOriginate => "CHANNEL_ORIGINATE",
169    ChannelUuid => "CHANNEL_UUID",
170    Api => "API",
171    Log => "LOG",
172    InboundChan => "INBOUND_CHAN",
173    OutboundChan => "OUTBOUND_CHAN",
174    Startup => "STARTUP",
175    Shutdown => "SHUTDOWN",
176    Publish => "PUBLISH",
177    Unpublish => "UNPUBLISH",
178    Talk => "TALK",
179    Notalk => "NOTALK",
180    SessionCrash => "SESSION_CRASH",
181    ModuleLoad => "MODULE_LOAD",
182    ModuleUnload => "MODULE_UNLOAD",
183    Dtmf => "DTMF",
184    Message => "MESSAGE",
185    PresenceIn => "PRESENCE_IN",
186    NotifyIn => "NOTIFY_IN",
187    PresenceOut => "PRESENCE_OUT",
188    PresenceProbe => "PRESENCE_PROBE",
189    MessageWaiting => "MESSAGE_WAITING",
190    MessageQuery => "MESSAGE_QUERY",
191    Roster => "ROSTER",
192    Codec => "CODEC",
193    BackgroundJob => "BACKGROUND_JOB",
194    DetectedSpeech => "DETECTED_SPEECH",
195    DetectedTone => "DETECTED_TONE",
196    PrivateCommand => "PRIVATE_COMMAND",
197    Heartbeat => "HEARTBEAT",
198    Trap => "TRAP",
199    AddSchedule => "ADD_SCHEDULE",
200    DelSchedule => "DEL_SCHEDULE",
201    ExeSchedule => "EXE_SCHEDULE",
202    ReSchedule => "RE_SCHEDULE",
203    ReloadXml => "RELOADXML",
204    Notify => "NOTIFY",
205    PhoneFeature => "PHONE_FEATURE",
206    PhoneFeatureSubscribe => "PHONE_FEATURE_SUBSCRIBE",
207    SendMessage => "SEND_MESSAGE",
208    RecvMessage => "RECV_MESSAGE",
209    RequestParams => "REQUEST_PARAMS",
210    ChannelData => "CHANNEL_DATA",
211    General => "GENERAL",
212    Command => "COMMAND",
213    SessionHeartbeat => "SESSION_HEARTBEAT",
214    ClientDisconnected => "CLIENT_DISCONNECTED",
215    ServerDisconnected => "SERVER_DISCONNECTED",
216    SendInfo => "SEND_INFO",
217    RecvInfo => "RECV_INFO",
218    RecvRtcpMessage => "RECV_RTCP_MESSAGE",
219    SendRtcpMessage => "SEND_RTCP_MESSAGE",
220    CallSecure => "CALL_SECURE",
221    Nat => "NAT",
222    RecordStart => "RECORD_START",
223    RecordStop => "RECORD_STOP",
224    PlaybackStart => "PLAYBACK_START",
225    PlaybackStop => "PLAYBACK_STOP",
226    CallUpdate => "CALL_UPDATE",
227    Failure => "FAILURE",
228    SocketData => "SOCKET_DATA",
229    MediaBugStart => "MEDIA_BUG_START",
230    MediaBugStop => "MEDIA_BUG_STOP",
231    ConferenceDataQuery => "CONFERENCE_DATA_QUERY",
232    ConferenceData => "CONFERENCE_DATA",
233    CallSetupReq => "CALL_SETUP_REQ",
234    CallSetupResult => "CALL_SETUP_RESULT",
235    CallDetail => "CALL_DETAIL",
236    DeviceState => "DEVICE_STATE",
237    Text => "TEXT",
238    ShutdownRequested => "SHUTDOWN_REQUESTED",
239    /// Subscribe to all events
240    All => "ALL";
241    // --- Not in libs/esl/ EVENT_NAMES[], only in switch_event.c ---
242    // check-event-types.sh stops scanning at the All variant above.
243    /// Present in `switch_event.c` but not in `libs/esl/` EVENT_NAMES[].
244    StartRecording => "START_RECORDING",
245}
246
247// -- Event group constants --------------------------------------------------
248//
249// Predefined slices for common subscription patterns. Pass directly to
250// `EslClient::subscribe_events()`.
251//
252// MAINTENANCE: when adding new `EslEventType` variants, check whether they
253// belong in any of these groups and update accordingly.
254
255impl EslEventType {
256    /// Every `CHANNEL_*` event type.
257    ///
258    /// Covers the full channel lifecycle: creation, state changes, execution,
259    /// bridging, hold, park, progress, originate, and destruction.
260    ///
261    /// ```rust
262    /// use freeswitch_types::EslEventType;
263    /// assert!(EslEventType::CHANNEL_EVENTS.contains(&EslEventType::ChannelCreate));
264    /// assert!(EslEventType::CHANNEL_EVENTS.contains(&EslEventType::ChannelHangupComplete));
265    /// ```
266    pub const CHANNEL_EVENTS: &[EslEventType] = &[
267        EslEventType::ChannelCreate,
268        EslEventType::ChannelDestroy,
269        EslEventType::ChannelState,
270        EslEventType::ChannelCallstate,
271        EslEventType::ChannelAnswer,
272        EslEventType::ChannelHangup,
273        EslEventType::ChannelHangupComplete,
274        EslEventType::ChannelExecute,
275        EslEventType::ChannelExecuteComplete,
276        EslEventType::ChannelHold,
277        EslEventType::ChannelUnhold,
278        EslEventType::ChannelBridge,
279        EslEventType::ChannelUnbridge,
280        EslEventType::ChannelProgress,
281        EslEventType::ChannelProgressMedia,
282        EslEventType::ChannelOutgoing,
283        EslEventType::ChannelPark,
284        EslEventType::ChannelUnpark,
285        EslEventType::ChannelApplication,
286        EslEventType::ChannelOriginate,
287        EslEventType::ChannelUuid,
288        EslEventType::ChannelData,
289    ];
290
291    /// In-call events: DTMF, VAD speech detection, media security, and call updates.
292    ///
293    /// Events that fire during an established call, tied to RTP/media activity
294    /// rather than signaling state transitions.
295    ///
296    /// ```rust
297    /// use freeswitch_types::EslEventType;
298    /// assert!(EslEventType::IN_CALL_EVENTS.contains(&EslEventType::Dtmf));
299    /// assert!(EslEventType::IN_CALL_EVENTS.contains(&EslEventType::Talk));
300    /// ```
301    pub const IN_CALL_EVENTS: &[EslEventType] = &[
302        EslEventType::Dtmf,
303        EslEventType::Talk,
304        EslEventType::Notalk,
305        EslEventType::CallSecure,
306        EslEventType::CallUpdate,
307        EslEventType::RecvRtcpMessage,
308        EslEventType::SendRtcpMessage,
309    ];
310
311    /// Media-related events: playback, recording, media bugs, and detection.
312    ///
313    /// Useful for IVR applications that need to track media operations without
314    /// subscribing to the full channel lifecycle.
315    ///
316    /// ```rust
317    /// use freeswitch_types::EslEventType;
318    /// assert!(EslEventType::MEDIA_EVENTS.contains(&EslEventType::PlaybackStart));
319    /// assert!(EslEventType::MEDIA_EVENTS.contains(&EslEventType::DetectedSpeech));
320    /// ```
321    pub const MEDIA_EVENTS: &[EslEventType] = &[
322        EslEventType::PlaybackStart,
323        EslEventType::PlaybackStop,
324        EslEventType::RecordStart,
325        EslEventType::RecordStop,
326        EslEventType::StartRecording,
327        EslEventType::MediaBugStart,
328        EslEventType::MediaBugStop,
329        EslEventType::DetectedSpeech,
330        EslEventType::DetectedTone,
331    ];
332
333    /// Presence and messaging events.
334    ///
335    /// For applications that track user presence (BLF, buddy lists) or
336    /// message-waiting indicators (voicemail MWI).
337    ///
338    /// ```rust
339    /// use freeswitch_types::EslEventType;
340    /// assert!(EslEventType::PRESENCE_EVENTS.contains(&EslEventType::PresenceIn));
341    /// assert!(EslEventType::PRESENCE_EVENTS.contains(&EslEventType::MessageWaiting));
342    /// ```
343    pub const PRESENCE_EVENTS: &[EslEventType] = &[
344        EslEventType::PresenceIn,
345        EslEventType::PresenceOut,
346        EslEventType::PresenceProbe,
347        EslEventType::MessageWaiting,
348        EslEventType::MessageQuery,
349        EslEventType::Roster,
350    ];
351
352    /// System lifecycle events.
353    ///
354    /// Server startup/shutdown, heartbeats, module loading, and XML reloads.
355    /// Useful for monitoring dashboards and operational tooling.
356    ///
357    /// ```rust
358    /// use freeswitch_types::EslEventType;
359    /// assert!(EslEventType::SYSTEM_EVENTS.contains(&EslEventType::Heartbeat));
360    /// assert!(EslEventType::SYSTEM_EVENTS.contains(&EslEventType::Shutdown));
361    /// ```
362    pub const SYSTEM_EVENTS: &[EslEventType] = &[
363        EslEventType::Startup,
364        EslEventType::Shutdown,
365        EslEventType::ShutdownRequested,
366        EslEventType::Heartbeat,
367        EslEventType::SessionHeartbeat,
368        EslEventType::SessionCrash,
369        EslEventType::ModuleLoad,
370        EslEventType::ModuleUnload,
371        EslEventType::ReloadXml,
372    ];
373
374    /// Conference-related events.
375    ///
376    /// ```rust
377    /// use freeswitch_types::EslEventType;
378    /// assert!(EslEventType::CONFERENCE_EVENTS.contains(&EslEventType::ConferenceData));
379    /// ```
380    pub const CONFERENCE_EVENTS: &[EslEventType] = &[
381        EslEventType::ConferenceDataQuery,
382        EslEventType::ConferenceData,
383    ];
384}
385
386/// Error returned when parsing an unknown event type string.
387#[derive(Debug, Clone, PartialEq, Eq)]
388pub struct ParseEventTypeError(pub String);
389
390impl fmt::Display for ParseEventTypeError {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        write!(f, "unknown event type: {}", self.0)
393    }
394}
395
396impl std::error::Error for ParseEventTypeError {}
397
398/// Error returned when an [`EventSubscription`] builder method receives invalid input.
399///
400/// Custom subclasses and filter values are validated against ESL wire-safety
401/// constraints: no newlines, carriage returns, or (for subclasses) spaces.
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct EventSubscriptionError(pub String);
404
405impl fmt::Display for EventSubscriptionError {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        write!(f, "invalid event subscription: {}", self.0)
408    }
409}
410
411impl std::error::Error for EventSubscriptionError {}
412
413/// Declarative description of an ESL event subscription.
414///
415/// Captures the event format, event types, custom subclasses, and filters
416/// as a single unit. Useful for config-driven subscriptions and reconnection
417/// patterns where the caller needs to rebuild subscriptions from a saved
418/// description.
419///
420/// # Wire safety
421///
422/// Builder methods validate inputs against ESL wire injection risks.
423/// Custom subclasses reject `\n`, `\r`, spaces, and empty strings.
424/// Filter headers and values reject `\n` and `\r`.
425///
426/// # Example
427///
428/// ```rust
429/// use freeswitch_types::{EventSubscription, EventFormat, EslEventType, EventHeader};
430///
431/// let sub = EventSubscription::new(EventFormat::Plain)
432///     .events(EslEventType::CHANNEL_EVENTS)
433///     .event(EslEventType::Heartbeat)
434///     .custom_subclass("sofia::register").unwrap()
435///     .filter(EventHeader::CallDirection, "inbound").unwrap();
436///
437/// assert!(!sub.is_empty());
438/// assert!(!sub.is_all());
439/// ```
440#[derive(Debug, Clone, PartialEq, Eq)]
441#[non_exhaustive]
442pub struct EventSubscription {
443    format: EventFormat,
444    events: Vec<EslEventType>,
445    custom_subclasses: Vec<String>,
446    filters: Vec<(String, String)>,
447}
448
449/// Validates that a custom subclass token is safe for ESL wire use.
450fn validate_custom_subclass(s: &str) -> Result<(), EventSubscriptionError> {
451    if s.is_empty() {
452        return Err(EventSubscriptionError(
453            "custom subclass cannot be empty".into(),
454        ));
455    }
456    if s.contains('\n') || s.contains('\r') {
457        return Err(EventSubscriptionError(format!(
458            "custom subclass contains newline: {:?}",
459            s
460        )));
461    }
462    if s.contains(' ') {
463        return Err(EventSubscriptionError(format!(
464            "custom subclass contains space: {:?}",
465            s
466        )));
467    }
468    Ok(())
469}
470
471/// Validates that a filter header or value has no newline characters.
472fn validate_filter_field(field: &str, label: &str) -> Result<(), EventSubscriptionError> {
473    if field.contains('\n') || field.contains('\r') {
474        return Err(EventSubscriptionError(format!(
475            "filter {} contains newline: {:?}",
476            label, field
477        )));
478    }
479    Ok(())
480}
481
482impl EventSubscription {
483    /// Create an empty subscription with the given format.
484    pub fn new(format: EventFormat) -> Self {
485        Self {
486            format,
487            events: Vec::new(),
488            custom_subclasses: Vec::new(),
489            filters: Vec::new(),
490        }
491    }
492
493    /// Create a subscription for all events.
494    pub fn all(format: EventFormat) -> Self {
495        Self {
496            format,
497            events: vec![EslEventType::All],
498            custom_subclasses: Vec::new(),
499            filters: Vec::new(),
500        }
501    }
502
503    /// Add a single event type.
504    pub fn event(mut self, event: EslEventType) -> Self {
505        self.events
506            .push(event);
507        self
508    }
509
510    /// Add multiple event types (e.g. from group constants like `EslEventType::CHANNEL_EVENTS`).
511    pub fn events<T: IntoIterator<Item = impl std::borrow::Borrow<EslEventType>>>(
512        mut self,
513        events: T,
514    ) -> Self {
515        self.events
516            .extend(
517                events
518                    .into_iter()
519                    .map(|e| *e.borrow()),
520            );
521        self
522    }
523
524    /// Add a custom subclass (e.g. `"sofia::register"`).
525    ///
526    /// Returns `Err` if the subclass contains spaces, newlines, or is empty.
527    pub fn custom_subclass(
528        mut self,
529        subclass: impl Into<String>,
530    ) -> Result<Self, EventSubscriptionError> {
531        let s = subclass.into();
532        validate_custom_subclass(&s)?;
533        self.custom_subclasses
534            .push(s);
535        Ok(self)
536    }
537
538    /// Add multiple custom subclasses.
539    ///
540    /// Returns `Err` on the first invalid subclass.
541    pub fn custom_subclasses(
542        mut self,
543        subclasses: impl IntoIterator<Item = impl Into<String>>,
544    ) -> Result<Self, EventSubscriptionError> {
545        for s in subclasses {
546            let s = s.into();
547            validate_custom_subclass(&s)?;
548            self.custom_subclasses
549                .push(s);
550        }
551        Ok(self)
552    }
553
554    /// Subscribe to a single Sofia event subclass.
555    ///
556    /// Convenience wrapper around [`custom_subclass()`](Self::custom_subclass) that
557    /// accepts a typed [`SofiaEventSubclass`] instead of a raw string.
558    pub fn sofia_event(mut self, subclass: SofiaEventSubclass) -> Self {
559        self.custom_subclasses
560            .push(
561                subclass
562                    .as_str()
563                    .to_string(),
564            );
565        self
566    }
567
568    /// Subscribe to multiple Sofia event subclasses.
569    pub fn sofia_events(
570        mut self,
571        subclasses: impl IntoIterator<Item = impl std::borrow::Borrow<SofiaEventSubclass>>,
572    ) -> Self {
573        self.custom_subclasses
574            .extend(
575                subclasses
576                    .into_iter()
577                    .map(|s| {
578                        s.borrow()
579                            .as_str()
580                            .to_string()
581                    }),
582            );
583        self
584    }
585
586    /// Add a filter with a typed header.
587    ///
588    /// The header enum is always valid; only the value is validated.
589    pub fn filter(
590        self,
591        header: crate::headers::EventHeader,
592        value: impl Into<String>,
593    ) -> Result<Self, EventSubscriptionError> {
594        let v = value.into();
595        validate_filter_field(&v, "value")?;
596        let mut s = self;
597        s.filters
598            .push((
599                header
600                    .as_str()
601                    .to_string(),
602                v,
603            ));
604        Ok(s)
605    }
606
607    /// Add a filter with raw header and value strings.
608    ///
609    /// Both header and value are validated against newline injection.
610    pub fn filter_raw(
611        self,
612        header: impl Into<String>,
613        value: impl Into<String>,
614    ) -> Result<Self, EventSubscriptionError> {
615        let h = header.into();
616        let v = value.into();
617        validate_filter_field(&h, "header")?;
618        validate_filter_field(&v, "value")?;
619        let mut s = self;
620        s.filters
621            .push((h, v));
622        Ok(s)
623    }
624
625    /// Change the event format.
626    pub fn with_format(mut self, format: EventFormat) -> Self {
627        self.format = format;
628        self
629    }
630
631    /// The event format.
632    pub fn format(&self) -> EventFormat {
633        self.format
634    }
635
636    /// Mutable reference to the event format.
637    pub fn format_mut(&mut self) -> &mut EventFormat {
638        &mut self.format
639    }
640
641    /// The subscribed event types.
642    pub fn event_types(&self) -> &[EslEventType] {
643        &self.events
644    }
645
646    /// Mutable access to the event types list.
647    pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
648        &mut self.events
649    }
650
651    /// The subscribed custom subclasses.
652    pub fn custom_subclass_list(&self) -> &[String] {
653        &self.custom_subclasses
654    }
655
656    /// Mutable access to the custom subclasses list.
657    pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
658        &mut self.custom_subclasses
659    }
660
661    /// The event filters as (header, value) pairs.
662    pub fn filters(&self) -> &[(String, String)] {
663        &self.filters
664    }
665
666    /// Mutable access to the filters list.
667    pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
668        &mut self.filters
669    }
670
671    /// Whether the subscription includes all events.
672    pub fn is_all(&self) -> bool {
673        self.events
674            .contains(&EslEventType::All)
675    }
676
677    /// Whether the subscription has no events and no custom subclasses.
678    pub fn is_empty(&self) -> bool {
679        self.events
680            .is_empty()
681            && self
682                .custom_subclasses
683                .is_empty()
684    }
685
686    /// Build the event string for the ESL `event` command.
687    ///
688    /// Returns `None` if no events or custom subclasses are configured.
689    /// Returns `Some("ALL")` if `EslEventType::All` is present.
690    /// Otherwise returns space-separated event names with custom subclasses
691    /// appended after a `CUSTOM` token.
692    pub fn to_event_string(&self) -> Option<String> {
693        if self
694            .events
695            .contains(&EslEventType::All)
696        {
697            return Some("ALL".to_string());
698        }
699
700        let mut parts: Vec<&str> = self
701            .events
702            .iter()
703            .map(|e| e.as_str())
704            .collect();
705
706        if !self
707            .custom_subclasses
708            .is_empty()
709        {
710            if !self
711                .events
712                .contains(&EslEventType::Custom)
713            {
714                parts.push("CUSTOM");
715            }
716            for sc in &self.custom_subclasses {
717                parts.push(sc.as_str());
718            }
719        }
720
721        if parts.is_empty() {
722            None
723        } else {
724            Some(parts.join(" "))
725        }
726    }
727}
728
729#[cfg(feature = "serde")]
730mod event_subscription_serde {
731    use super::*;
732    use serde::{Deserialize, Serialize};
733
734    #[derive(Serialize, Deserialize)]
735    struct EventSubscriptionRaw {
736        format: EventFormat,
737        #[serde(default)]
738        events: Vec<EslEventType>,
739        #[serde(default)]
740        custom_subclasses: Vec<String>,
741        #[serde(default)]
742        filters: Vec<(String, String)>,
743    }
744
745    impl TryFrom<EventSubscriptionRaw> for EventSubscription {
746        type Error = EventSubscriptionError;
747
748        fn try_from(raw: EventSubscriptionRaw) -> Result<Self, Self::Error> {
749            for sc in &raw.custom_subclasses {
750                validate_custom_subclass(sc)?;
751            }
752            for (h, v) in &raw.filters {
753                validate_filter_field(h, "header")?;
754                validate_filter_field(v, "value")?;
755            }
756            Ok(EventSubscription {
757                format: raw.format,
758                events: raw.events,
759                custom_subclasses: raw.custom_subclasses,
760                filters: raw.filters,
761            })
762        }
763    }
764
765    impl Serialize for EventSubscription {
766        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
767            let raw = EventSubscriptionRaw {
768                format: self.format,
769                events: self
770                    .events
771                    .clone(),
772                custom_subclasses: self
773                    .custom_subclasses
774                    .clone(),
775                filters: self
776                    .filters
777                    .clone(),
778            };
779            raw.serialize(serializer)
780        }
781    }
782
783    impl<'de> Deserialize<'de> for EventSubscription {
784        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
785            let raw = EventSubscriptionRaw::deserialize(deserializer)?;
786            EventSubscription::try_from(raw).map_err(serde::de::Error::custom)
787        }
788    }
789}
790
791/// Event priority levels matching FreeSWITCH `esl_priority_t`
792#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
793#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
794#[non_exhaustive]
795pub enum EslEventPriority {
796    /// Default priority.
797    Normal,
798    /// Lower than normal.
799    Low,
800    /// Higher than normal.
801    High,
802}
803
804impl fmt::Display for EslEventPriority {
805    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806        match self {
807            EslEventPriority::Normal => write!(f, "NORMAL"),
808            EslEventPriority::Low => write!(f, "LOW"),
809            EslEventPriority::High => write!(f, "HIGH"),
810        }
811    }
812}
813
814/// Error returned when parsing an invalid priority string.
815#[derive(Debug, Clone, PartialEq, Eq)]
816pub struct ParsePriorityError(pub String);
817
818impl fmt::Display for ParsePriorityError {
819    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820        write!(f, "unknown priority: {}", self.0)
821    }
822}
823
824impl std::error::Error for ParsePriorityError {}
825
826impl FromStr for EslEventPriority {
827    type Err = ParsePriorityError;
828
829    fn from_str(s: &str) -> Result<Self, Self::Err> {
830        match s {
831            "NORMAL" => Ok(EslEventPriority::Normal),
832            "LOW" => Ok(EslEventPriority::Low),
833            "HIGH" => Ok(EslEventPriority::High),
834            _ => Err(ParsePriorityError(s.to_string())),
835        }
836    }
837}
838
839/// ESL Event structure containing headers and optional body
840#[derive(Debug, Clone, Eq)]
841#[cfg_attr(feature = "serde", derive(serde::Serialize))]
842pub struct EslEvent {
843    event_type: Option<EslEventType>,
844    headers: IndexMap<String, String>,
845    #[cfg_attr(feature = "serde", serde(skip))]
846    original_keys: IndexMap<String, String>,
847    body: Option<String>,
848}
849
850impl EslEvent {
851    /// Create a new empty event
852    pub fn new() -> Self {
853        Self {
854            event_type: None,
855            headers: IndexMap::new(),
856            original_keys: IndexMap::new(),
857            body: None,
858        }
859    }
860
861    /// Create event with specified type
862    pub fn with_type(event_type: EslEventType) -> Self {
863        Self {
864            event_type: Some(event_type),
865            headers: IndexMap::new(),
866            original_keys: IndexMap::new(),
867            body: None,
868        }
869    }
870
871    /// Parsed event type, if recognized.
872    pub fn event_type(&self) -> Option<EslEventType> {
873        self.event_type
874    }
875
876    /// Override the event type.
877    pub fn set_event_type(&mut self, event_type: Option<EslEventType>) {
878        self.event_type = event_type;
879    }
880
881    /// Look up a header by its [`EventHeader`] enum variant (case-sensitive).
882    ///
883    /// For headers not covered by `EventHeader`, use [`header_str()`](Self::header_str).
884    pub fn header(&self, name: EventHeader) -> Option<&str> {
885        self.headers
886            .get(name.as_str())
887            .map(|s| s.as_str())
888    }
889
890    /// Look up a header by name, trying the canonical key first then falling
891    /// back through the alias map for non-canonical lookups.
892    ///
893    /// Use [`header()`](Self::header) with an [`EventHeader`] variant for known
894    /// headers. This method is for headers not (yet) covered by the enum,
895    /// such as custom `X-` headers or FreeSWITCH headers added after this
896    /// library was published.
897    pub fn header_str(&self, name: &str) -> Option<&str> {
898        self.headers
899            .get(name)
900            .or_else(|| {
901                self.original_keys
902                    .get(name)
903                    .and_then(|normalized| {
904                        self.headers
905                            .get(normalized)
906                    })
907            })
908            .map(|s| s.as_str())
909    }
910
911    /// Look up a channel variable by its bare name.
912    ///
913    /// Equivalent to [`variable()`](Self::variable) but matches the
914    /// [`HeaderLookup`] trait signature.
915    pub fn variable_str(&self, name: &str) -> Option<&str> {
916        let key = format!("variable_{}", name);
917        self.header_str(&key)
918    }
919
920    /// All headers as a map.
921    pub fn headers(&self) -> &IndexMap<String, String> {
922        &self.headers
923    }
924
925    /// Set or overwrite a header, normalizing the key.
926    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
927        let original = name.into();
928        let normalized = normalize_header_key(&original);
929        if original != normalized {
930            self.original_keys
931                .insert(original, normalized.clone());
932        }
933        self.headers
934            .insert(normalized, value.into());
935    }
936
937    /// Remove a header, returning its value if it existed.
938    ///
939    /// Accepts both canonical and original (non-normalized) key names.
940    pub fn remove_header(&mut self, name: impl AsRef<str>) -> Option<String> {
941        let name = name.as_ref();
942        if let Some(value) = self
943            .headers
944            .shift_remove(name)
945        {
946            return Some(value);
947        }
948        if let Some(normalized) = self
949            .original_keys
950            .shift_remove(name)
951        {
952            return self
953                .headers
954                .shift_remove(&normalized);
955        }
956        None
957    }
958
959    /// Event body (the content after the blank line in plain-text events).
960    pub fn body(&self) -> Option<&str> {
961        self.body
962            .as_deref()
963    }
964
965    /// Set the event body.
966    pub fn set_body(&mut self, body: impl Into<String>) {
967        self.body = Some(body.into());
968    }
969
970    /// Sets the `priority` header carried on the event.
971    ///
972    /// FreeSWITCH stores this as metadata but does **not** use it for dispatch
973    /// ordering -- all events are delivered FIFO regardless of priority.
974    pub fn set_priority(&mut self, priority: EslEventPriority) {
975        self.set_header(EventHeader::Priority.as_str(), priority.to_string());
976    }
977
978    /// Append a value to a multi-value header (PUSH semantics).
979    ///
980    /// If the header doesn't exist, sets it as a plain value.
981    /// If it exists as a plain value, converts to `ARRAY::old|:new`.
982    /// If it already has an `ARRAY::` prefix, appends the new value.
983    ///
984    /// Returns [`EslArrayError::TooManyItems`] if the existing header already
985    /// contains [`MAX_ARRAY_ITEMS`](crate::MAX_ARRAY_ITEMS) items.
986    ///
987    /// ```
988    /// # use freeswitch_types::EslEvent;
989    /// let mut event = EslEvent::new();
990    /// event.push_header("X-Test", "first").unwrap();
991    /// event.push_header("X-Test", "second").unwrap();
992    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
993    /// ```
994    pub fn push_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
995        self.stack_header(name, value, EslArray::push)
996    }
997
998    /// Prepend a value to a multi-value header (UNSHIFT semantics).
999    ///
1000    /// Same conversion rules as [`push_header()`](Self::push_header), but
1001    /// inserts at the front.
1002    ///
1003    /// ```
1004    /// # use freeswitch_types::EslEvent;
1005    /// let mut event = EslEvent::new();
1006    /// event.set_header("X-Test", "ARRAY::b|:c");
1007    /// event.unshift_header("X-Test", "a").unwrap();
1008    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1009    /// ```
1010    pub fn unshift_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
1011        self.stack_header(name, value, EslArray::unshift)
1012    }
1013
1014    fn stack_header(
1015        &mut self,
1016        name: &str,
1017        value: &str,
1018        op: fn(&mut EslArray, String),
1019    ) -> Result<(), EslArrayError> {
1020        match self
1021            .headers
1022            .get(name)
1023        {
1024            None => {
1025                self.set_header(name, value);
1026            }
1027            Some(existing) => {
1028                let arr = match EslArray::parse(existing) {
1029                    Ok(arr) => arr,
1030                    Err(EslArrayError::MissingPrefix) => EslArray::new(vec![existing.clone()]),
1031                    Err(e) => return Err(e),
1032                };
1033                if arr.len() >= crate::variables::MAX_ARRAY_ITEMS {
1034                    return Err(EslArrayError::TooManyItems {
1035                        count: arr.len(),
1036                        max: crate::variables::MAX_ARRAY_ITEMS,
1037                    });
1038                }
1039                let mut arr = arr;
1040                op(&mut arr, value.into());
1041                self.set_header(name, arr.to_string());
1042            }
1043        }
1044        Ok(())
1045    }
1046
1047    /// Check whether this event matches the given type.
1048    pub fn is_event_type(&self, event_type: EslEventType) -> bool {
1049        self.event_type == Some(event_type)
1050    }
1051
1052    /// Serialize to ESL plain text wire format with percent-encoded header values.
1053    ///
1054    /// This is the inverse of `EslParser::parse_plain_event()`. The output can
1055    /// be fed back through the parser to reconstruct an equivalent `EslEvent`
1056    /// (round-trip).
1057    ///
1058    /// Headers are emitted in insertion order (which matches wire order when the
1059    /// event was parsed from the network). `Content-Length` from stored headers
1060    /// is skipped and recomputed from the body if present.
1061    pub fn to_plain_format(&self) -> String {
1062        use std::fmt::Write;
1063        let mut result = String::new();
1064
1065        for (key, value) in &self.headers {
1066            if key == "Content-Length" {
1067                continue;
1068            }
1069            let _ = writeln!(
1070                result,
1071                "{}: {}",
1072                key,
1073                percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
1074            );
1075        }
1076
1077        if let Some(body) = &self.body {
1078            let _ = writeln!(result, "Content-Length: {}", body.len());
1079            result.push('\n');
1080            result.push_str(body);
1081        } else {
1082            result.push('\n');
1083        }
1084
1085        result
1086    }
1087}
1088
1089impl Default for EslEvent {
1090    fn default() -> Self {
1091        Self::new()
1092    }
1093}
1094
1095impl HeaderLookup for EslEvent {
1096    fn header_str(&self, name: &str) -> Option<&str> {
1097        EslEvent::header_str(self, name)
1098    }
1099
1100    fn variable_str(&self, name: &str) -> Option<&str> {
1101        let key = format!("variable_{}", name);
1102        self.header_str(&key)
1103    }
1104}
1105
1106impl PartialEq for EslEvent {
1107    fn eq(&self, other: &Self) -> bool {
1108        self.event_type == other.event_type
1109            && self.headers == other.headers
1110            && self.body == other.body
1111    }
1112}
1113
1114impl std::hash::Hash for EslEvent {
1115    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1116        self.event_type
1117            .hash(state);
1118        for (k, v) in &self.headers {
1119            k.hash(state);
1120            v.hash(state);
1121        }
1122        self.body
1123            .hash(state);
1124    }
1125}
1126
1127#[cfg(feature = "serde")]
1128impl<'de> serde::Deserialize<'de> for EslEvent {
1129    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1130    where
1131        D: serde::Deserializer<'de>,
1132    {
1133        #[derive(serde::Deserialize)]
1134        struct Raw {
1135            event_type: Option<EslEventType>,
1136            headers: IndexMap<String, String>,
1137            body: Option<String>,
1138        }
1139        let raw = Raw::deserialize(deserializer)?;
1140        let mut event = EslEvent::new();
1141        event.event_type = raw.event_type;
1142        event.body = raw.body;
1143        for (k, v) in raw.headers {
1144            event.set_header(k, v);
1145        }
1146        Ok(event)
1147    }
1148}
1149
1150#[cfg(test)]
1151mod tests {
1152    use super::*;
1153
1154    #[test]
1155    fn headers_preserve_insertion_order() {
1156        let mut event = EslEvent::new();
1157        event.set_header("Zebra", "last");
1158        event.set_header("Alpha", "first");
1159        event.set_header("Middle", "mid");
1160        let keys: Vec<&str> = event
1161            .headers()
1162            .keys()
1163            .map(|s| s.as_str())
1164            .collect();
1165        assert_eq!(keys, vec!["Zebra", "Alpha", "Middle"]);
1166    }
1167
1168    #[test]
1169    fn test_notify_in_parse() {
1170        assert_eq!(
1171            EslEventType::parse_event_type("NOTIFY_IN"),
1172            Some(EslEventType::NotifyIn)
1173        );
1174        assert_eq!(EslEventType::parse_event_type("notify_in"), None);
1175    }
1176
1177    #[test]
1178    fn test_notify_in_display() {
1179        assert_eq!(EslEventType::NotifyIn.to_string(), "NOTIFY_IN");
1180    }
1181
1182    #[test]
1183    fn test_notify_in_distinct_from_notify() {
1184        assert_ne!(EslEventType::Notify, EslEventType::NotifyIn);
1185        assert_ne!(
1186            EslEventType::Notify.to_string(),
1187            EslEventType::NotifyIn.to_string()
1188        );
1189    }
1190
1191    #[test]
1192    fn test_wire_names_match_c_esl() {
1193        assert_eq!(
1194            EslEventType::ChannelOutgoing.to_string(),
1195            "CHANNEL_OUTGOING"
1196        );
1197        assert_eq!(EslEventType::Api.to_string(), "API");
1198        assert_eq!(EslEventType::ReloadXml.to_string(), "RELOADXML");
1199        assert_eq!(EslEventType::PresenceIn.to_string(), "PRESENCE_IN");
1200        assert_eq!(EslEventType::Roster.to_string(), "ROSTER");
1201        assert_eq!(EslEventType::Text.to_string(), "TEXT");
1202        assert_eq!(EslEventType::ReSchedule.to_string(), "RE_SCHEDULE");
1203
1204        assert_eq!(
1205            EslEventType::parse_event_type("CHANNEL_OUTGOING"),
1206            Some(EslEventType::ChannelOutgoing)
1207        );
1208        assert_eq!(
1209            EslEventType::parse_event_type("API"),
1210            Some(EslEventType::Api)
1211        );
1212        assert_eq!(
1213            EslEventType::parse_event_type("RELOADXML"),
1214            Some(EslEventType::ReloadXml)
1215        );
1216        assert_eq!(
1217            EslEventType::parse_event_type("PRESENCE_IN"),
1218            Some(EslEventType::PresenceIn)
1219        );
1220    }
1221
1222    #[test]
1223    fn test_event_type_from_str() {
1224        assert_eq!(
1225            "CHANNEL_ANSWER".parse::<EslEventType>(),
1226            Ok(EslEventType::ChannelAnswer)
1227        );
1228        assert!("channel_answer"
1229            .parse::<EslEventType>()
1230            .is_err());
1231        assert!("UNKNOWN_EVENT"
1232            .parse::<EslEventType>()
1233            .is_err());
1234    }
1235
1236    #[test]
1237    fn test_remove_header() {
1238        let mut event = EslEvent::new();
1239        event.set_header("Foo", "bar");
1240        event.set_header("Baz", "qux");
1241
1242        let removed = event.remove_header("Foo");
1243        assert_eq!(removed, Some("bar".to_string()));
1244        assert!(event
1245            .header_str("Foo")
1246            .is_none());
1247        assert_eq!(event.header_str("Baz"), Some("qux"));
1248
1249        let removed_again = event.remove_header("Foo");
1250        assert_eq!(removed_again, None);
1251    }
1252
1253    #[test]
1254    fn test_to_plain_format_basic() {
1255        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1256        event.set_header("Event-Name", "HEARTBEAT");
1257        event.set_header("Core-UUID", "abc-123");
1258
1259        let plain = event.to_plain_format();
1260
1261        assert!(plain.starts_with("Event-Name: "));
1262        assert!(plain.contains("Core-UUID: "));
1263        assert!(plain.ends_with("\n\n"));
1264    }
1265
1266    #[test]
1267    fn test_to_plain_format_percent_encoding() {
1268        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1269        event.set_header("Event-Name", "HEARTBEAT");
1270        event.set_header("Up-Time", "0 years, 0 days");
1271
1272        let plain = event.to_plain_format();
1273
1274        assert!(!plain.contains("0 years, 0 days"));
1275        assert!(plain.contains("Up-Time: "));
1276        assert!(plain.contains("%20"));
1277    }
1278
1279    #[test]
1280    fn test_to_plain_format_with_body() {
1281        let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
1282        event.set_header("Event-Name", "BACKGROUND_JOB");
1283        event.set_header("Job-UUID", "def-456");
1284        event.set_body("+OK result\n".to_string());
1285
1286        let plain = event.to_plain_format();
1287
1288        assert!(plain.contains("Content-Length: 11\n"));
1289        assert!(plain.ends_with("\n\n+OK result\n"));
1290    }
1291
1292    #[test]
1293    fn test_to_plain_format_preserves_insertion_order() {
1294        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1295        event.set_header("Event-Name", "HEARTBEAT");
1296        event.set_header("Core-UUID", "abc-123");
1297        event.set_header("FreeSWITCH-Hostname", "fs01");
1298        event.set_header("Up-Time", "0 years, 1 day");
1299
1300        let plain = event.to_plain_format();
1301        let lines: Vec<&str> = plain
1302            .lines()
1303            .collect();
1304        assert!(lines[0].starts_with("Event-Name: "));
1305        assert!(lines[1].starts_with("Core-UUID: "));
1306        assert!(lines[2].starts_with("FreeSWITCH-Hostname: "));
1307        assert!(lines[3].starts_with("Up-Time: "));
1308    }
1309
1310    #[test]
1311    fn test_to_plain_format_round_trip() {
1312        let mut original = EslEvent::with_type(EslEventType::ChannelCreate);
1313        original.set_header("Event-Name", "CHANNEL_CREATE");
1314        original.set_header("Core-UUID", "abc-123");
1315        original.set_header("Channel-Name", "sofia/internal/1000@example.com");
1316        original.set_header("Caller-Caller-ID-Name", "Jérôme Poulin");
1317        original.set_body("some body content");
1318
1319        let plain = original.to_plain_format();
1320
1321        // Simulate what EslParser::parse_plain_event does
1322        let (header_section, inner_body) = if let Some(pos) = plain.find("\n\n") {
1323            (&plain[..pos], Some(&plain[pos + 2..]))
1324        } else {
1325            (plain.as_str(), None)
1326        };
1327
1328        let mut parsed = EslEvent::new();
1329        for line in header_section.lines() {
1330            let line = line.trim();
1331            if line.is_empty() {
1332                continue;
1333            }
1334            if let Some(colon_pos) = line.find(':') {
1335                let key = line[..colon_pos].trim();
1336                if key == "Content-Length" {
1337                    continue;
1338                }
1339                let raw_value = line[colon_pos + 1..].trim();
1340                let value = percent_encoding::percent_decode_str(raw_value)
1341                    .decode_utf8()
1342                    .unwrap()
1343                    .into_owned();
1344                parsed.set_header(key, value);
1345            }
1346        }
1347        if let Some(ib) = inner_body {
1348            if !ib.is_empty() {
1349                parsed.set_body(ib);
1350            }
1351        }
1352
1353        assert_eq!(original.headers(), parsed.headers());
1354        assert_eq!(original.body(), parsed.body());
1355    }
1356
1357    #[test]
1358    fn test_set_priority_normal() {
1359        let mut event = EslEvent::new();
1360        event.set_priority(EslEventPriority::Normal);
1361        assert_eq!(
1362            event
1363                .priority()
1364                .unwrap(),
1365            Some(EslEventPriority::Normal)
1366        );
1367        assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
1368    }
1369
1370    #[test]
1371    fn test_set_priority_high() {
1372        let mut event = EslEvent::new();
1373        event.set_priority(EslEventPriority::High);
1374        assert_eq!(
1375            event
1376                .priority()
1377                .unwrap(),
1378            Some(EslEventPriority::High)
1379        );
1380        assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
1381    }
1382
1383    #[test]
1384    fn test_priority_display() {
1385        assert_eq!(EslEventPriority::Normal.to_string(), "NORMAL");
1386        assert_eq!(EslEventPriority::Low.to_string(), "LOW");
1387        assert_eq!(EslEventPriority::High.to_string(), "HIGH");
1388    }
1389
1390    #[test]
1391    fn test_priority_from_str() {
1392        assert_eq!(
1393            "NORMAL".parse::<EslEventPriority>(),
1394            Ok(EslEventPriority::Normal)
1395        );
1396        assert_eq!("LOW".parse::<EslEventPriority>(), Ok(EslEventPriority::Low));
1397        assert_eq!(
1398            "HIGH".parse::<EslEventPriority>(),
1399            Ok(EslEventPriority::High)
1400        );
1401        assert!("INVALID"
1402            .parse::<EslEventPriority>()
1403            .is_err());
1404    }
1405
1406    #[test]
1407    fn test_priority_from_str_rejects_wrong_case() {
1408        assert!("normal"
1409            .parse::<EslEventPriority>()
1410            .is_err());
1411        assert!("Low"
1412            .parse::<EslEventPriority>()
1413            .is_err());
1414        assert!("hIgH"
1415            .parse::<EslEventPriority>()
1416            .is_err());
1417    }
1418
1419    #[test]
1420    fn test_push_header_new() {
1421        let mut event = EslEvent::new();
1422        event
1423            .push_header("X-Test", "first")
1424            .unwrap();
1425        assert_eq!(event.header_str("X-Test"), Some("first"));
1426    }
1427
1428    #[test]
1429    fn test_push_header_existing_plain() {
1430        let mut event = EslEvent::new();
1431        event.set_header("X-Test", "first");
1432        event
1433            .push_header("X-Test", "second")
1434            .unwrap();
1435        assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
1436    }
1437
1438    #[test]
1439    fn test_push_header_existing_array() {
1440        let mut event = EslEvent::new();
1441        event.set_header("X-Test", "ARRAY::a|:b");
1442        event
1443            .push_header("X-Test", "c")
1444            .unwrap();
1445        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1446    }
1447
1448    #[test]
1449    fn test_push_header_at_capacity() {
1450        use crate::variables::MAX_ARRAY_ITEMS;
1451        let mut event = EslEvent::new();
1452        let items: Vec<&str> = (0..MAX_ARRAY_ITEMS)
1453            .map(|_| "x")
1454            .collect();
1455        event.set_header("X-Test", format!("ARRAY::{}", items.join("|:")).as_str());
1456        assert!(matches!(
1457            event.push_header("X-Test", "overflow"),
1458            Err(EslArrayError::TooManyItems { .. })
1459        ));
1460    }
1461
1462    #[test]
1463    fn test_unshift_header_new() {
1464        let mut event = EslEvent::new();
1465        event
1466            .unshift_header("X-Test", "only")
1467            .unwrap();
1468        assert_eq!(event.header_str("X-Test"), Some("only"));
1469    }
1470
1471    #[test]
1472    fn test_unshift_header_existing_array() {
1473        let mut event = EslEvent::new();
1474        event.set_header("X-Test", "ARRAY::b|:c");
1475        event
1476            .unshift_header("X-Test", "a")
1477            .unwrap();
1478        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1479    }
1480
1481    #[test]
1482    fn test_sendevent_with_priority_wire_format() {
1483        let mut event = EslEvent::with_type(EslEventType::Custom);
1484        event.set_header("Event-Name", "CUSTOM");
1485        event.set_header("Event-Subclass", "test::priority");
1486        event.set_priority(EslEventPriority::High);
1487
1488        let plain = event.to_plain_format();
1489        assert!(plain.contains("priority: HIGH\n"));
1490    }
1491
1492    #[test]
1493    fn test_convenience_accessors() {
1494        let mut event = EslEvent::new();
1495        event.set_header("Channel-Name", "sofia/internal/1000@example.com");
1496        event.set_header("Caller-Caller-ID-Number", "1000");
1497        event.set_header("Caller-Caller-ID-Name", "Alice");
1498        event.set_header("Hangup-Cause", "NORMAL_CLEARING");
1499        event.set_header("Event-Subclass", "sofia::register");
1500        event.set_header("variable_sip_from_display", "Bob");
1501
1502        assert_eq!(
1503            event.channel_name(),
1504            Some("sofia/internal/1000@example.com")
1505        );
1506        assert_eq!(event.caller_id_number(), Some("1000"));
1507        assert_eq!(event.caller_id_name(), Some("Alice"));
1508        assert_eq!(
1509            event
1510                .hangup_cause()
1511                .unwrap(),
1512            Some(crate::channel::HangupCause::NormalClearing)
1513        );
1514        assert_eq!(event.event_subclass(), Some("sofia::register"));
1515        assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
1516        assert_eq!(event.variable_str("nonexistent"), None);
1517    }
1518
1519    #[test]
1520    fn test_event_format_from_str() {
1521        assert_eq!("plain".parse::<EventFormat>(), Ok(EventFormat::Plain));
1522        assert_eq!("json".parse::<EventFormat>(), Ok(EventFormat::Json));
1523        assert_eq!("xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1524        assert!("foo"
1525            .parse::<EventFormat>()
1526            .is_err());
1527    }
1528
1529    #[test]
1530    fn test_event_format_from_str_case_insensitive() {
1531        assert_eq!("PLAIN".parse::<EventFormat>(), Ok(EventFormat::Plain));
1532        assert_eq!("Json".parse::<EventFormat>(), Ok(EventFormat::Json));
1533        assert_eq!("XML".parse::<EventFormat>(), Ok(EventFormat::Xml));
1534        assert_eq!("Xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1535    }
1536
1537    #[test]
1538    fn test_event_format_from_content_type() {
1539        assert_eq!(
1540            EventFormat::from_content_type("text/event-json"),
1541            Ok(EventFormat::Json)
1542        );
1543        assert_eq!(
1544            EventFormat::from_content_type("text/event-xml"),
1545            Ok(EventFormat::Xml)
1546        );
1547        assert_eq!(
1548            EventFormat::from_content_type("text/event-plain"),
1549            Ok(EventFormat::Plain)
1550        );
1551        assert!(EventFormat::from_content_type("unknown").is_err());
1552    }
1553
1554    // --- EslEvent accessor tests (via HeaderLookup trait) ---
1555
1556    #[test]
1557    fn test_event_channel_state_accessor() {
1558        use crate::channel::ChannelState;
1559        let mut event = EslEvent::new();
1560        event.set_header("Channel-State", "CS_EXECUTE");
1561        assert_eq!(
1562            event
1563                .channel_state()
1564                .unwrap(),
1565            Some(ChannelState::CsExecute)
1566        );
1567    }
1568
1569    #[test]
1570    fn test_event_channel_state_number_accessor() {
1571        use crate::channel::ChannelState;
1572        let mut event = EslEvent::new();
1573        event.set_header("Channel-State-Number", "4");
1574        assert_eq!(
1575            event
1576                .channel_state_number()
1577                .unwrap(),
1578            Some(ChannelState::CsExecute)
1579        );
1580    }
1581
1582    #[test]
1583    fn test_event_call_state_accessor() {
1584        use crate::channel::CallState;
1585        let mut event = EslEvent::new();
1586        event.set_header("Channel-Call-State", "ACTIVE");
1587        assert_eq!(
1588            event
1589                .call_state()
1590                .unwrap(),
1591            Some(CallState::Active)
1592        );
1593    }
1594
1595    #[test]
1596    fn test_event_answer_state_accessor() {
1597        use crate::channel::AnswerState;
1598        let mut event = EslEvent::new();
1599        event.set_header("Answer-State", "answered");
1600        assert_eq!(
1601            event
1602                .answer_state()
1603                .unwrap(),
1604            Some(AnswerState::Answered)
1605        );
1606    }
1607
1608    #[test]
1609    fn test_event_call_direction_accessor() {
1610        use crate::channel::CallDirection;
1611        let mut event = EslEvent::new();
1612        event.set_header("Call-Direction", "inbound");
1613        assert_eq!(
1614            event
1615                .call_direction()
1616                .unwrap(),
1617            Some(CallDirection::Inbound)
1618        );
1619    }
1620
1621    #[test]
1622    fn test_event_typed_accessors_missing_headers() {
1623        let event = EslEvent::new();
1624        assert_eq!(
1625            event
1626                .channel_state()
1627                .unwrap(),
1628            None
1629        );
1630        assert_eq!(
1631            event
1632                .channel_state_number()
1633                .unwrap(),
1634            None
1635        );
1636        assert_eq!(
1637            event
1638                .call_state()
1639                .unwrap(),
1640            None
1641        );
1642        assert_eq!(
1643            event
1644                .answer_state()
1645                .unwrap(),
1646            None
1647        );
1648        assert_eq!(
1649            event
1650                .call_direction()
1651                .unwrap(),
1652            None
1653        );
1654    }
1655
1656    // --- Repeating SIP header tests ---
1657
1658    #[test]
1659    fn test_sip_p_asserted_identity_comma_separated() {
1660        let mut event = EslEvent::new();
1661        // RFC 3325: P-Asserted-Identity can carry two identities (one sip:, one tel:)
1662        // FreeSWITCH stores the comma-separated value as a single channel variable
1663        event.set_header(
1664            "variable_sip_P-Asserted-Identity",
1665            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1666        );
1667
1668        assert_eq!(
1669            event.variable_str("sip_P-Asserted-Identity"),
1670            Some("<sip:alice@atlanta.example.com>, <tel:+15551234567>")
1671        );
1672    }
1673
1674    #[test]
1675    fn test_sip_p_asserted_identity_array_format() {
1676        let mut event = EslEvent::new();
1677        // When FreeSWITCH stores repeated SIP headers via ARRAY format
1678        event
1679            .push_header(
1680                "variable_sip_P-Asserted-Identity",
1681                "<sip:alice@atlanta.example.com>",
1682            )
1683            .unwrap();
1684        event
1685            .push_header("variable_sip_P-Asserted-Identity", "<tel:+15551234567>")
1686            .unwrap();
1687
1688        let raw = event
1689            .header_str("variable_sip_P-Asserted-Identity")
1690            .unwrap();
1691        assert_eq!(
1692            raw,
1693            "ARRAY::<sip:alice@atlanta.example.com>|:<tel:+15551234567>"
1694        );
1695
1696        let arr = crate::variables::EslArray::parse(raw).unwrap();
1697        assert_eq!(arr.len(), 2);
1698        assert_eq!(arr.items()[0], "<sip:alice@atlanta.example.com>");
1699        assert_eq!(arr.items()[1], "<tel:+15551234567>");
1700    }
1701
1702    #[test]
1703    fn test_sip_header_with_colons_in_uri() {
1704        let mut event = EslEvent::new();
1705        // SIP URIs contain colons (sip:, sips:) which must not confuse ARRAY parsing
1706        event
1707            .push_header(
1708                "variable_sip_h_Diversion",
1709                "<sip:+15551234567@gw.example.com;reason=unconditional>",
1710            )
1711            .unwrap();
1712        event
1713            .push_header(
1714                "variable_sip_h_Diversion",
1715                "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>",
1716            )
1717            .unwrap();
1718
1719        let raw = event
1720            .header_str("variable_sip_h_Diversion")
1721            .unwrap();
1722        let arr = crate::variables::EslArray::parse(raw).unwrap();
1723        assert_eq!(arr.len(), 2);
1724        assert_eq!(
1725            arr.items()[0],
1726            "<sip:+15551234567@gw.example.com;reason=unconditional>"
1727        );
1728        assert_eq!(
1729            arr.items()[1],
1730            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>"
1731        );
1732    }
1733
1734    #[test]
1735    fn test_sip_p_asserted_identity_plain_format_round_trip() {
1736        let mut event = EslEvent::with_type(EslEventType::ChannelCreate);
1737        event.set_header("Event-Name", "CHANNEL_CREATE");
1738        event.set_header(
1739            "variable_sip_P-Asserted-Identity",
1740            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1741        );
1742
1743        let plain = event.to_plain_format();
1744        // The comma-separated value should be percent-encoded on the wire
1745        assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
1746        // Angle brackets and comma should be encoded
1747        assert!(!plain.contains("<sip:alice"));
1748    }
1749
1750    // --- Header key normalization on EslEvent ---
1751    // set_header() normalizes keys so lookups via header(EventHeader::X)
1752    // and header_str() work regardless of the casing used at insertion.
1753
1754    #[test]
1755    fn set_header_normalizes_known_enum_variant() {
1756        let mut event = EslEvent::new();
1757        event.set_header("unique-id", "abc-123");
1758        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1759    }
1760
1761    #[test]
1762    fn set_header_normalizes_codec_header() {
1763        let mut event = EslEvent::new();
1764        event.set_header("channel-read-codec-bit-rate", "128000");
1765        assert_eq!(
1766            event.header(EventHeader::ChannelReadCodecBitRate),
1767            Some("128000")
1768        );
1769    }
1770
1771    #[test]
1772    fn header_str_finds_by_original_key() {
1773        let mut event = EslEvent::new();
1774        event.set_header("unique-id", "abc-123");
1775        // Lookup by original non-canonical key should still work
1776        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1777        // Lookup by canonical key also works
1778        assert_eq!(event.header_str("Unique-ID"), Some("abc-123"));
1779    }
1780
1781    #[test]
1782    fn header_str_finds_unknown_dash_header_by_original() {
1783        let mut event = EslEvent::new();
1784        event.set_header("x-custom-header", "val");
1785        // Stored as Title-Case
1786        assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
1787        // Original key also works via alias
1788        assert_eq!(event.header_str("x-custom-header"), Some("val"));
1789    }
1790
1791    #[test]
1792    fn set_header_underscore_passthrough_preserves_sip_h() {
1793        let mut event = EslEvent::new();
1794        event.set_header("variable_sip_h_X-My-CUSTOM-Header", "val");
1795        assert_eq!(
1796            event.header_str("variable_sip_h_X-My-CUSTOM-Header"),
1797            Some("val")
1798        );
1799    }
1800
1801    #[test]
1802    fn set_header_different_casing_overwrites() {
1803        let mut event = EslEvent::new();
1804        event.set_header("Unique-ID", "first");
1805        event.set_header("unique-id", "second");
1806        // Both normalize to "Unique-ID", second overwrites first
1807        assert_eq!(event.header(EventHeader::UniqueId), Some("second"));
1808    }
1809
1810    #[test]
1811    fn remove_header_by_original_key() {
1812        let mut event = EslEvent::new();
1813        event.set_header("unique-id", "abc-123");
1814        let removed = event.remove_header("unique-id");
1815        assert_eq!(removed, Some("abc-123".to_string()));
1816        assert_eq!(event.header(EventHeader::UniqueId), None);
1817    }
1818
1819    #[test]
1820    fn remove_header_by_canonical_key() {
1821        let mut event = EslEvent::new();
1822        event.set_header("unique-id", "abc-123");
1823        let removed = event.remove_header("Unique-ID");
1824        assert_eq!(removed, Some("abc-123".to_string()));
1825        assert_eq!(event.header_str("unique-id"), None);
1826    }
1827
1828    #[test]
1829    fn serde_round_trip_preserves_canonical_lookups() {
1830        let mut event = EslEvent::new();
1831        event.set_header("unique-id", "abc-123");
1832        event.set_header("channel-read-codec-bit-rate", "128000");
1833        let json = serde_json::to_string(&event).unwrap();
1834        let deserialized: EslEvent = serde_json::from_str(&json).unwrap();
1835        assert_eq!(deserialized.header(EventHeader::UniqueId), Some("abc-123"));
1836        assert_eq!(
1837            deserialized.header(EventHeader::ChannelReadCodecBitRate),
1838            Some("128000")
1839        );
1840    }
1841
1842    #[test]
1843    fn serde_deserialize_normalizes_external_json() {
1844        let json = r#"{"event_type":null,"headers":{"unique-id":"abc-123","channel-read-codec-bit-rate":"128000"},"body":null}"#;
1845        let event: EslEvent = serde_json::from_str(json).unwrap();
1846        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1847        assert_eq!(
1848            event.header(EventHeader::ChannelReadCodecBitRate),
1849            Some("128000")
1850        );
1851        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1852    }
1853
1854    #[test]
1855    fn test_event_typed_accessors_invalid_values() {
1856        let mut event = EslEvent::new();
1857        event.set_header("Channel-State", "BOGUS");
1858        event.set_header("Channel-State-Number", "999");
1859        event.set_header("Channel-Call-State", "BOGUS");
1860        event.set_header("Answer-State", "bogus");
1861        event.set_header("Call-Direction", "bogus");
1862        assert!(event
1863            .channel_state()
1864            .is_err());
1865        assert!(event
1866            .channel_state_number()
1867            .is_err());
1868        assert!(event
1869            .call_state()
1870            .is_err());
1871        assert!(event
1872            .answer_state()
1873            .is_err());
1874        assert!(event
1875            .call_direction()
1876            .is_err());
1877    }
1878
1879    // --- EventSubscription tests ---
1880
1881    #[test]
1882    fn new_creates_empty() {
1883        let sub = EventSubscription::new(EventFormat::Plain);
1884        assert!(sub.is_empty());
1885        assert!(!sub.is_all());
1886        assert_eq!(sub.format(), EventFormat::Plain);
1887        assert!(sub
1888            .event_types()
1889            .is_empty());
1890        assert!(sub
1891            .custom_subclass_list()
1892            .is_empty());
1893        assert!(sub
1894            .filters()
1895            .is_empty());
1896    }
1897
1898    #[test]
1899    fn all_creates_all() {
1900        let sub = EventSubscription::all(EventFormat::Json);
1901        assert!(sub.is_all());
1902        assert!(!sub.is_empty());
1903        assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
1904    }
1905
1906    #[test]
1907    fn event_string_typed_only() {
1908        let sub = EventSubscription::new(EventFormat::Plain)
1909            .event(EslEventType::ChannelCreate)
1910            .event(EslEventType::ChannelAnswer);
1911        assert_eq!(
1912            sub.to_event_string(),
1913            Some("CHANNEL_CREATE CHANNEL_ANSWER".to_string())
1914        );
1915    }
1916
1917    #[test]
1918    fn event_string_custom_only() {
1919        let sub = EventSubscription::new(EventFormat::Plain)
1920            .custom_subclass("sofia::register")
1921            .unwrap()
1922            .custom_subclass("sofia::unregister")
1923            .unwrap();
1924        assert_eq!(
1925            sub.to_event_string(),
1926            Some("CUSTOM sofia::register sofia::unregister".to_string())
1927        );
1928    }
1929
1930    #[test]
1931    fn event_string_mixed() {
1932        let sub = EventSubscription::new(EventFormat::Plain)
1933            .event(EslEventType::Heartbeat)
1934            .custom_subclass("sofia::register")
1935            .unwrap();
1936        assert_eq!(
1937            sub.to_event_string(),
1938            Some("HEARTBEAT CUSTOM sofia::register".to_string())
1939        );
1940    }
1941
1942    #[test]
1943    fn event_string_custom_not_duplicated() {
1944        let sub = EventSubscription::new(EventFormat::Plain)
1945            .event(EslEventType::Custom)
1946            .custom_subclass("sofia::register")
1947            .unwrap();
1948        // Should not have "CUSTOM" twice
1949        assert_eq!(
1950            sub.to_event_string(),
1951            Some("CUSTOM sofia::register".to_string())
1952        );
1953    }
1954
1955    #[test]
1956    fn event_string_empty_is_none() {
1957        let sub = EventSubscription::new(EventFormat::Plain);
1958        assert_eq!(sub.to_event_string(), None);
1959    }
1960
1961    #[test]
1962    fn filters_preserve_order() {
1963        let sub = EventSubscription::new(EventFormat::Plain)
1964            .filter(EventHeader::CallDirection, "inbound")
1965            .unwrap()
1966            .filter_raw("X-Custom", "value1")
1967            .unwrap()
1968            .filter(EventHeader::ChannelState, "CS_EXECUTE")
1969            .unwrap();
1970        assert_eq!(
1971            sub.filters(),
1972            &[
1973                ("Call-Direction".to_string(), "inbound".to_string()),
1974                ("X-Custom".to_string(), "value1".to_string()),
1975                ("Channel-State".to_string(), "CS_EXECUTE".to_string()),
1976            ]
1977        );
1978    }
1979
1980    #[test]
1981    fn builder_chain() {
1982        let sub = EventSubscription::new(EventFormat::Plain)
1983            .events(EslEventType::CHANNEL_EVENTS)
1984            .event(EslEventType::Heartbeat)
1985            .custom_subclass("sofia::register")
1986            .unwrap()
1987            .filter(EventHeader::CallDirection, "inbound")
1988            .unwrap()
1989            .with_format(EventFormat::Json);
1990
1991        assert_eq!(sub.format(), EventFormat::Json);
1992        assert!(!sub.is_empty());
1993        assert!(!sub.is_all());
1994        assert!(sub
1995            .event_types()
1996            .contains(&EslEventType::ChannelCreate));
1997        assert!(sub
1998            .event_types()
1999            .contains(&EslEventType::Heartbeat));
2000        assert_eq!(sub.custom_subclass_list(), &["sofia::register"]);
2001        assert_eq!(
2002            sub.filters()
2003                .len(),
2004            1
2005        );
2006    }
2007
2008    #[test]
2009    fn serde_round_trip_subscription() {
2010        let sub = EventSubscription::new(EventFormat::Plain)
2011            .event(EslEventType::ChannelCreate)
2012            .event(EslEventType::Heartbeat)
2013            .custom_subclass("sofia::register")
2014            .unwrap()
2015            .filter(EventHeader::CallDirection, "inbound")
2016            .unwrap();
2017
2018        let json = serde_json::to_string(&sub).unwrap();
2019        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
2020        assert_eq!(sub, deserialized);
2021    }
2022
2023    #[test]
2024    fn serde_rejects_invalid_subclass() {
2025        let json =
2026            r#"{"format":"Plain","events":[],"custom_subclasses":["bad subclass"],"filters":[]}"#;
2027        let result: Result<EventSubscription, _> = serde_json::from_str(json);
2028        assert!(result.is_err());
2029        let err = result
2030            .unwrap_err()
2031            .to_string();
2032        assert!(err.contains("space"), "error should mention space: {err}");
2033    }
2034
2035    #[test]
2036    fn serde_rejects_newline_in_filter() {
2037        let json = r#"{"format":"Plain","events":[],"custom_subclasses":[],"filters":[["Header","val\n"]]}"#;
2038        let result: Result<EventSubscription, _> = serde_json::from_str(json);
2039        assert!(result.is_err());
2040        let err = result
2041            .unwrap_err()
2042            .to_string();
2043        assert!(
2044            err.contains("newline"),
2045            "error should mention newline: {err}"
2046        );
2047    }
2048
2049    #[test]
2050    fn custom_subclass_rejects_space() {
2051        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad subclass");
2052        assert!(result.is_err());
2053    }
2054
2055    #[test]
2056    fn custom_subclass_rejects_newline() {
2057        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad\nsubclass");
2058        assert!(result.is_err());
2059    }
2060
2061    #[test]
2062    fn custom_subclass_rejects_empty() {
2063        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("");
2064        assert!(result.is_err());
2065    }
2066
2067    #[test]
2068    fn filter_raw_rejects_newline_in_header() {
2069        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Bad\nHeader", "value");
2070        assert!(result.is_err());
2071    }
2072
2073    #[test]
2074    fn filter_raw_rejects_newline_in_value() {
2075        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Header", "bad\nvalue");
2076        assert!(result.is_err());
2077    }
2078
2079    #[test]
2080    fn filter_typed_rejects_newline_in_value() {
2081        let result = EventSubscription::new(EventFormat::Plain)
2082            .filter(EventHeader::CallDirection, "bad\nvalue");
2083        assert!(result.is_err());
2084    }
2085
2086    #[test]
2087    fn sofia_event_single() {
2088        let sub =
2089            EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
2090        assert_eq!(
2091            sub.to_event_string(),
2092            Some("CUSTOM sofia::register".to_string())
2093        );
2094    }
2095
2096    #[test]
2097    fn sofia_events_group() {
2098        let sub = EventSubscription::new(EventFormat::Plain)
2099            .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
2100        let event_str = sub
2101            .to_event_string()
2102            .unwrap();
2103        assert!(event_str.starts_with("CUSTOM"));
2104        assert!(event_str.contains("sofia::gateway_state"));
2105        assert!(event_str.contains("sofia::gateway_add"));
2106        assert!(event_str.contains("sofia::gateway_delete"));
2107        assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
2108    }
2109
2110    #[test]
2111    fn sofia_event_mixed_with_typed_events() {
2112        let sub = EventSubscription::new(EventFormat::Plain)
2113            .event(EslEventType::Heartbeat)
2114            .sofia_event(SofiaEventSubclass::GatewayState);
2115        assert_eq!(
2116            sub.to_event_string(),
2117            Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
2118        );
2119    }
2120}