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;
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    /// ```
985    /// # use freeswitch_types::EslEvent;
986    /// let mut event = EslEvent::new();
987    /// event.push_header("X-Test", "first");
988    /// event.push_header("X-Test", "second");
989    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
990    /// ```
991    pub fn push_header(&mut self, name: &str, value: &str) {
992        self.stack_header(name, value, EslArray::push);
993    }
994
995    /// Prepend a value to a multi-value header (UNSHIFT semantics).
996    ///
997    /// Same conversion rules as `push_header()`, but inserts at the front.
998    ///
999    /// ```
1000    /// # use freeswitch_types::EslEvent;
1001    /// let mut event = EslEvent::new();
1002    /// event.set_header("X-Test", "ARRAY::b|:c");
1003    /// event.unshift_header("X-Test", "a");
1004    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1005    /// ```
1006    pub fn unshift_header(&mut self, name: &str, value: &str) {
1007        self.stack_header(name, value, EslArray::unshift);
1008    }
1009
1010    fn stack_header(&mut self, name: &str, value: &str, op: fn(&mut EslArray, String)) {
1011        match self
1012            .headers
1013            .get(name)
1014        {
1015            None => {
1016                self.set_header(name, value);
1017            }
1018            Some(existing) => {
1019                let mut arr = match EslArray::parse(existing) {
1020                    Some(arr) => arr,
1021                    None => EslArray::new(vec![existing.clone()]),
1022                };
1023                op(&mut arr, value.into());
1024                self.set_header(name, arr.to_string());
1025            }
1026        }
1027    }
1028
1029    /// Check whether this event matches the given type.
1030    pub fn is_event_type(&self, event_type: EslEventType) -> bool {
1031        self.event_type == Some(event_type)
1032    }
1033
1034    /// Serialize to ESL plain text wire format with percent-encoded header values.
1035    ///
1036    /// This is the inverse of `EslParser::parse_plain_event()`. The output can
1037    /// be fed back through the parser to reconstruct an equivalent `EslEvent`
1038    /// (round-trip).
1039    ///
1040    /// Headers are emitted in insertion order (which matches wire order when the
1041    /// event was parsed from the network). `Content-Length` from stored headers
1042    /// is skipped and recomputed from the body if present.
1043    pub fn to_plain_format(&self) -> String {
1044        use std::fmt::Write;
1045        let mut result = String::new();
1046
1047        for (key, value) in &self.headers {
1048            if key == "Content-Length" {
1049                continue;
1050            }
1051            let _ = writeln!(
1052                result,
1053                "{}: {}",
1054                key,
1055                percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
1056            );
1057        }
1058
1059        if let Some(body) = &self.body {
1060            let _ = writeln!(result, "Content-Length: {}", body.len());
1061            result.push('\n');
1062            result.push_str(body);
1063        } else {
1064            result.push('\n');
1065        }
1066
1067        result
1068    }
1069}
1070
1071impl Default for EslEvent {
1072    fn default() -> Self {
1073        Self::new()
1074    }
1075}
1076
1077impl HeaderLookup for EslEvent {
1078    fn header_str(&self, name: &str) -> Option<&str> {
1079        EslEvent::header_str(self, name)
1080    }
1081
1082    fn variable_str(&self, name: &str) -> Option<&str> {
1083        let key = format!("variable_{}", name);
1084        self.header_str(&key)
1085    }
1086}
1087
1088impl PartialEq for EslEvent {
1089    fn eq(&self, other: &Self) -> bool {
1090        self.event_type == other.event_type
1091            && self.headers == other.headers
1092            && self.body == other.body
1093    }
1094}
1095
1096impl std::hash::Hash for EslEvent {
1097    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1098        self.event_type
1099            .hash(state);
1100        for (k, v) in &self.headers {
1101            k.hash(state);
1102            v.hash(state);
1103        }
1104        self.body
1105            .hash(state);
1106    }
1107}
1108
1109#[cfg(feature = "serde")]
1110impl<'de> serde::Deserialize<'de> for EslEvent {
1111    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1112    where
1113        D: serde::Deserializer<'de>,
1114    {
1115        #[derive(serde::Deserialize)]
1116        struct Raw {
1117            event_type: Option<EslEventType>,
1118            headers: IndexMap<String, String>,
1119            body: Option<String>,
1120        }
1121        let raw = Raw::deserialize(deserializer)?;
1122        let mut event = EslEvent::new();
1123        event.event_type = raw.event_type;
1124        event.body = raw.body;
1125        for (k, v) in raw.headers {
1126            event.set_header(k, v);
1127        }
1128        Ok(event)
1129    }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use super::*;
1135
1136    #[test]
1137    fn headers_preserve_insertion_order() {
1138        let mut event = EslEvent::new();
1139        event.set_header("Zebra", "last");
1140        event.set_header("Alpha", "first");
1141        event.set_header("Middle", "mid");
1142        let keys: Vec<&str> = event
1143            .headers()
1144            .keys()
1145            .map(|s| s.as_str())
1146            .collect();
1147        assert_eq!(keys, vec!["Zebra", "Alpha", "Middle"]);
1148    }
1149
1150    #[test]
1151    fn test_notify_in_parse() {
1152        assert_eq!(
1153            EslEventType::parse_event_type("NOTIFY_IN"),
1154            Some(EslEventType::NotifyIn)
1155        );
1156        assert_eq!(EslEventType::parse_event_type("notify_in"), None);
1157    }
1158
1159    #[test]
1160    fn test_notify_in_display() {
1161        assert_eq!(EslEventType::NotifyIn.to_string(), "NOTIFY_IN");
1162    }
1163
1164    #[test]
1165    fn test_notify_in_distinct_from_notify() {
1166        assert_ne!(EslEventType::Notify, EslEventType::NotifyIn);
1167        assert_ne!(
1168            EslEventType::Notify.to_string(),
1169            EslEventType::NotifyIn.to_string()
1170        );
1171    }
1172
1173    #[test]
1174    fn test_wire_names_match_c_esl() {
1175        assert_eq!(
1176            EslEventType::ChannelOutgoing.to_string(),
1177            "CHANNEL_OUTGOING"
1178        );
1179        assert_eq!(EslEventType::Api.to_string(), "API");
1180        assert_eq!(EslEventType::ReloadXml.to_string(), "RELOADXML");
1181        assert_eq!(EslEventType::PresenceIn.to_string(), "PRESENCE_IN");
1182        assert_eq!(EslEventType::Roster.to_string(), "ROSTER");
1183        assert_eq!(EslEventType::Text.to_string(), "TEXT");
1184        assert_eq!(EslEventType::ReSchedule.to_string(), "RE_SCHEDULE");
1185
1186        assert_eq!(
1187            EslEventType::parse_event_type("CHANNEL_OUTGOING"),
1188            Some(EslEventType::ChannelOutgoing)
1189        );
1190        assert_eq!(
1191            EslEventType::parse_event_type("API"),
1192            Some(EslEventType::Api)
1193        );
1194        assert_eq!(
1195            EslEventType::parse_event_type("RELOADXML"),
1196            Some(EslEventType::ReloadXml)
1197        );
1198        assert_eq!(
1199            EslEventType::parse_event_type("PRESENCE_IN"),
1200            Some(EslEventType::PresenceIn)
1201        );
1202    }
1203
1204    #[test]
1205    fn test_event_type_from_str() {
1206        assert_eq!(
1207            "CHANNEL_ANSWER".parse::<EslEventType>(),
1208            Ok(EslEventType::ChannelAnswer)
1209        );
1210        assert!("channel_answer"
1211            .parse::<EslEventType>()
1212            .is_err());
1213        assert!("UNKNOWN_EVENT"
1214            .parse::<EslEventType>()
1215            .is_err());
1216    }
1217
1218    #[test]
1219    fn test_remove_header() {
1220        let mut event = EslEvent::new();
1221        event.set_header("Foo", "bar");
1222        event.set_header("Baz", "qux");
1223
1224        let removed = event.remove_header("Foo");
1225        assert_eq!(removed, Some("bar".to_string()));
1226        assert!(event
1227            .header_str("Foo")
1228            .is_none());
1229        assert_eq!(event.header_str("Baz"), Some("qux"));
1230
1231        let removed_again = event.remove_header("Foo");
1232        assert_eq!(removed_again, None);
1233    }
1234
1235    #[test]
1236    fn test_to_plain_format_basic() {
1237        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1238        event.set_header("Event-Name", "HEARTBEAT");
1239        event.set_header("Core-UUID", "abc-123");
1240
1241        let plain = event.to_plain_format();
1242
1243        assert!(plain.starts_with("Event-Name: "));
1244        assert!(plain.contains("Core-UUID: "));
1245        assert!(plain.ends_with("\n\n"));
1246    }
1247
1248    #[test]
1249    fn test_to_plain_format_percent_encoding() {
1250        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1251        event.set_header("Event-Name", "HEARTBEAT");
1252        event.set_header("Up-Time", "0 years, 0 days");
1253
1254        let plain = event.to_plain_format();
1255
1256        assert!(!plain.contains("0 years, 0 days"));
1257        assert!(plain.contains("Up-Time: "));
1258        assert!(plain.contains("%20"));
1259    }
1260
1261    #[test]
1262    fn test_to_plain_format_with_body() {
1263        let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
1264        event.set_header("Event-Name", "BACKGROUND_JOB");
1265        event.set_header("Job-UUID", "def-456");
1266        event.set_body("+OK result\n".to_string());
1267
1268        let plain = event.to_plain_format();
1269
1270        assert!(plain.contains("Content-Length: 11\n"));
1271        assert!(plain.ends_with("\n\n+OK result\n"));
1272    }
1273
1274    #[test]
1275    fn test_to_plain_format_preserves_insertion_order() {
1276        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1277        event.set_header("Event-Name", "HEARTBEAT");
1278        event.set_header("Core-UUID", "abc-123");
1279        event.set_header("FreeSWITCH-Hostname", "fs01");
1280        event.set_header("Up-Time", "0 years, 1 day");
1281
1282        let plain = event.to_plain_format();
1283        let lines: Vec<&str> = plain
1284            .lines()
1285            .collect();
1286        assert!(lines[0].starts_with("Event-Name: "));
1287        assert!(lines[1].starts_with("Core-UUID: "));
1288        assert!(lines[2].starts_with("FreeSWITCH-Hostname: "));
1289        assert!(lines[3].starts_with("Up-Time: "));
1290    }
1291
1292    #[test]
1293    fn test_to_plain_format_round_trip() {
1294        let mut original = EslEvent::with_type(EslEventType::ChannelCreate);
1295        original.set_header("Event-Name", "CHANNEL_CREATE");
1296        original.set_header("Core-UUID", "abc-123");
1297        original.set_header("Channel-Name", "sofia/internal/1000@example.com");
1298        original.set_header("Caller-Caller-ID-Name", "Jérôme Poulin");
1299        original.set_body("some body content");
1300
1301        let plain = original.to_plain_format();
1302
1303        // Simulate what EslParser::parse_plain_event does
1304        let (header_section, inner_body) = if let Some(pos) = plain.find("\n\n") {
1305            (&plain[..pos], Some(&plain[pos + 2..]))
1306        } else {
1307            (plain.as_str(), None)
1308        };
1309
1310        let mut parsed = EslEvent::new();
1311        for line in header_section.lines() {
1312            let line = line.trim();
1313            if line.is_empty() {
1314                continue;
1315            }
1316            if let Some(colon_pos) = line.find(':') {
1317                let key = line[..colon_pos].trim();
1318                if key == "Content-Length" {
1319                    continue;
1320                }
1321                let raw_value = line[colon_pos + 1..].trim();
1322                let value = percent_encoding::percent_decode_str(raw_value)
1323                    .decode_utf8()
1324                    .unwrap()
1325                    .into_owned();
1326                parsed.set_header(key, value);
1327            }
1328        }
1329        if let Some(ib) = inner_body {
1330            if !ib.is_empty() {
1331                parsed.set_body(ib);
1332            }
1333        }
1334
1335        assert_eq!(original.headers(), parsed.headers());
1336        assert_eq!(original.body(), parsed.body());
1337    }
1338
1339    #[test]
1340    fn test_set_priority_normal() {
1341        let mut event = EslEvent::new();
1342        event.set_priority(EslEventPriority::Normal);
1343        assert_eq!(
1344            event
1345                .priority()
1346                .unwrap(),
1347            Some(EslEventPriority::Normal)
1348        );
1349        assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
1350    }
1351
1352    #[test]
1353    fn test_set_priority_high() {
1354        let mut event = EslEvent::new();
1355        event.set_priority(EslEventPriority::High);
1356        assert_eq!(
1357            event
1358                .priority()
1359                .unwrap(),
1360            Some(EslEventPriority::High)
1361        );
1362        assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
1363    }
1364
1365    #[test]
1366    fn test_priority_display() {
1367        assert_eq!(EslEventPriority::Normal.to_string(), "NORMAL");
1368        assert_eq!(EslEventPriority::Low.to_string(), "LOW");
1369        assert_eq!(EslEventPriority::High.to_string(), "HIGH");
1370    }
1371
1372    #[test]
1373    fn test_priority_from_str() {
1374        assert_eq!(
1375            "NORMAL".parse::<EslEventPriority>(),
1376            Ok(EslEventPriority::Normal)
1377        );
1378        assert_eq!("LOW".parse::<EslEventPriority>(), Ok(EslEventPriority::Low));
1379        assert_eq!(
1380            "HIGH".parse::<EslEventPriority>(),
1381            Ok(EslEventPriority::High)
1382        );
1383        assert!("INVALID"
1384            .parse::<EslEventPriority>()
1385            .is_err());
1386    }
1387
1388    #[test]
1389    fn test_priority_from_str_rejects_wrong_case() {
1390        assert!("normal"
1391            .parse::<EslEventPriority>()
1392            .is_err());
1393        assert!("Low"
1394            .parse::<EslEventPriority>()
1395            .is_err());
1396        assert!("hIgH"
1397            .parse::<EslEventPriority>()
1398            .is_err());
1399    }
1400
1401    #[test]
1402    fn test_push_header_new() {
1403        let mut event = EslEvent::new();
1404        event.push_header("X-Test", "first");
1405        assert_eq!(event.header_str("X-Test"), Some("first"));
1406    }
1407
1408    #[test]
1409    fn test_push_header_existing_plain() {
1410        let mut event = EslEvent::new();
1411        event.set_header("X-Test", "first");
1412        event.push_header("X-Test", "second");
1413        assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
1414    }
1415
1416    #[test]
1417    fn test_push_header_existing_array() {
1418        let mut event = EslEvent::new();
1419        event.set_header("X-Test", "ARRAY::a|:b");
1420        event.push_header("X-Test", "c");
1421        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1422    }
1423
1424    #[test]
1425    fn test_unshift_header_new() {
1426        let mut event = EslEvent::new();
1427        event.unshift_header("X-Test", "only");
1428        assert_eq!(event.header_str("X-Test"), Some("only"));
1429    }
1430
1431    #[test]
1432    fn test_unshift_header_existing_array() {
1433        let mut event = EslEvent::new();
1434        event.set_header("X-Test", "ARRAY::b|:c");
1435        event.unshift_header("X-Test", "a");
1436        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1437    }
1438
1439    #[test]
1440    fn test_sendevent_with_priority_wire_format() {
1441        let mut event = EslEvent::with_type(EslEventType::Custom);
1442        event.set_header("Event-Name", "CUSTOM");
1443        event.set_header("Event-Subclass", "test::priority");
1444        event.set_priority(EslEventPriority::High);
1445
1446        let plain = event.to_plain_format();
1447        assert!(plain.contains("priority: HIGH\n"));
1448    }
1449
1450    #[test]
1451    fn test_convenience_accessors() {
1452        let mut event = EslEvent::new();
1453        event.set_header("Channel-Name", "sofia/internal/1000@example.com");
1454        event.set_header("Caller-Caller-ID-Number", "1000");
1455        event.set_header("Caller-Caller-ID-Name", "Alice");
1456        event.set_header("Hangup-Cause", "NORMAL_CLEARING");
1457        event.set_header("Event-Subclass", "sofia::register");
1458        event.set_header("variable_sip_from_display", "Bob");
1459
1460        assert_eq!(
1461            event.channel_name(),
1462            Some("sofia/internal/1000@example.com")
1463        );
1464        assert_eq!(event.caller_id_number(), Some("1000"));
1465        assert_eq!(event.caller_id_name(), Some("Alice"));
1466        assert_eq!(
1467            event
1468                .hangup_cause()
1469                .unwrap(),
1470            Some(crate::channel::HangupCause::NormalClearing)
1471        );
1472        assert_eq!(event.event_subclass(), Some("sofia::register"));
1473        assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
1474        assert_eq!(event.variable_str("nonexistent"), None);
1475    }
1476
1477    #[test]
1478    fn test_event_format_from_str() {
1479        assert_eq!("plain".parse::<EventFormat>(), Ok(EventFormat::Plain));
1480        assert_eq!("json".parse::<EventFormat>(), Ok(EventFormat::Json));
1481        assert_eq!("xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1482        assert!("foo"
1483            .parse::<EventFormat>()
1484            .is_err());
1485    }
1486
1487    #[test]
1488    fn test_event_format_from_str_case_insensitive() {
1489        assert_eq!("PLAIN".parse::<EventFormat>(), Ok(EventFormat::Plain));
1490        assert_eq!("Json".parse::<EventFormat>(), Ok(EventFormat::Json));
1491        assert_eq!("XML".parse::<EventFormat>(), Ok(EventFormat::Xml));
1492        assert_eq!("Xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1493    }
1494
1495    #[test]
1496    fn test_event_format_from_content_type() {
1497        assert_eq!(
1498            EventFormat::from_content_type("text/event-json"),
1499            Ok(EventFormat::Json)
1500        );
1501        assert_eq!(
1502            EventFormat::from_content_type("text/event-xml"),
1503            Ok(EventFormat::Xml)
1504        );
1505        assert_eq!(
1506            EventFormat::from_content_type("text/event-plain"),
1507            Ok(EventFormat::Plain)
1508        );
1509        assert!(EventFormat::from_content_type("unknown").is_err());
1510    }
1511
1512    // --- EslEvent accessor tests (via HeaderLookup trait) ---
1513
1514    #[test]
1515    fn test_event_channel_state_accessor() {
1516        use crate::channel::ChannelState;
1517        let mut event = EslEvent::new();
1518        event.set_header("Channel-State", "CS_EXECUTE");
1519        assert_eq!(
1520            event
1521                .channel_state()
1522                .unwrap(),
1523            Some(ChannelState::CsExecute)
1524        );
1525    }
1526
1527    #[test]
1528    fn test_event_channel_state_number_accessor() {
1529        use crate::channel::ChannelState;
1530        let mut event = EslEvent::new();
1531        event.set_header("Channel-State-Number", "4");
1532        assert_eq!(
1533            event
1534                .channel_state_number()
1535                .unwrap(),
1536            Some(ChannelState::CsExecute)
1537        );
1538    }
1539
1540    #[test]
1541    fn test_event_call_state_accessor() {
1542        use crate::channel::CallState;
1543        let mut event = EslEvent::new();
1544        event.set_header("Channel-Call-State", "ACTIVE");
1545        assert_eq!(
1546            event
1547                .call_state()
1548                .unwrap(),
1549            Some(CallState::Active)
1550        );
1551    }
1552
1553    #[test]
1554    fn test_event_answer_state_accessor() {
1555        use crate::channel::AnswerState;
1556        let mut event = EslEvent::new();
1557        event.set_header("Answer-State", "answered");
1558        assert_eq!(
1559            event
1560                .answer_state()
1561                .unwrap(),
1562            Some(AnswerState::Answered)
1563        );
1564    }
1565
1566    #[test]
1567    fn test_event_call_direction_accessor() {
1568        use crate::channel::CallDirection;
1569        let mut event = EslEvent::new();
1570        event.set_header("Call-Direction", "inbound");
1571        assert_eq!(
1572            event
1573                .call_direction()
1574                .unwrap(),
1575            Some(CallDirection::Inbound)
1576        );
1577    }
1578
1579    #[test]
1580    fn test_event_typed_accessors_missing_headers() {
1581        let event = EslEvent::new();
1582        assert_eq!(
1583            event
1584                .channel_state()
1585                .unwrap(),
1586            None
1587        );
1588        assert_eq!(
1589            event
1590                .channel_state_number()
1591                .unwrap(),
1592            None
1593        );
1594        assert_eq!(
1595            event
1596                .call_state()
1597                .unwrap(),
1598            None
1599        );
1600        assert_eq!(
1601            event
1602                .answer_state()
1603                .unwrap(),
1604            None
1605        );
1606        assert_eq!(
1607            event
1608                .call_direction()
1609                .unwrap(),
1610            None
1611        );
1612    }
1613
1614    // --- Repeating SIP header tests ---
1615
1616    #[test]
1617    fn test_sip_p_asserted_identity_comma_separated() {
1618        let mut event = EslEvent::new();
1619        // RFC 3325: P-Asserted-Identity can carry two identities (one sip:, one tel:)
1620        // FreeSWITCH stores the comma-separated value as a single channel variable
1621        event.set_header(
1622            "variable_sip_P-Asserted-Identity",
1623            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1624        );
1625
1626        assert_eq!(
1627            event.variable_str("sip_P-Asserted-Identity"),
1628            Some("<sip:alice@atlanta.example.com>, <tel:+15551234567>")
1629        );
1630    }
1631
1632    #[test]
1633    fn test_sip_p_asserted_identity_array_format() {
1634        let mut event = EslEvent::new();
1635        // When FreeSWITCH stores repeated SIP headers via ARRAY format
1636        event.push_header(
1637            "variable_sip_P-Asserted-Identity",
1638            "<sip:alice@atlanta.example.com>",
1639        );
1640        event.push_header("variable_sip_P-Asserted-Identity", "<tel:+15551234567>");
1641
1642        let raw = event
1643            .header_str("variable_sip_P-Asserted-Identity")
1644            .unwrap();
1645        assert_eq!(
1646            raw,
1647            "ARRAY::<sip:alice@atlanta.example.com>|:<tel:+15551234567>"
1648        );
1649
1650        let arr = crate::variables::EslArray::parse(raw).unwrap();
1651        assert_eq!(arr.len(), 2);
1652        assert_eq!(arr.items()[0], "<sip:alice@atlanta.example.com>");
1653        assert_eq!(arr.items()[1], "<tel:+15551234567>");
1654    }
1655
1656    #[test]
1657    fn test_sip_header_with_colons_in_uri() {
1658        let mut event = EslEvent::new();
1659        // SIP URIs contain colons (sip:, sips:) which must not confuse ARRAY parsing
1660        event.push_header(
1661            "variable_sip_h_Diversion",
1662            "<sip:+15551234567@gw.example.com;reason=unconditional>",
1663        );
1664        event.push_header(
1665            "variable_sip_h_Diversion",
1666            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>",
1667        );
1668
1669        let raw = event
1670            .header_str("variable_sip_h_Diversion")
1671            .unwrap();
1672        let arr = crate::variables::EslArray::parse(raw).unwrap();
1673        assert_eq!(arr.len(), 2);
1674        assert_eq!(
1675            arr.items()[0],
1676            "<sip:+15551234567@gw.example.com;reason=unconditional>"
1677        );
1678        assert_eq!(
1679            arr.items()[1],
1680            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>"
1681        );
1682    }
1683
1684    #[test]
1685    fn test_sip_p_asserted_identity_plain_format_round_trip() {
1686        let mut event = EslEvent::with_type(EslEventType::ChannelCreate);
1687        event.set_header("Event-Name", "CHANNEL_CREATE");
1688        event.set_header(
1689            "variable_sip_P-Asserted-Identity",
1690            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1691        );
1692
1693        let plain = event.to_plain_format();
1694        // The comma-separated value should be percent-encoded on the wire
1695        assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
1696        // Angle brackets and comma should be encoded
1697        assert!(!plain.contains("<sip:alice"));
1698    }
1699
1700    // --- Header key normalization on EslEvent ---
1701    // set_header() normalizes keys so lookups via header(EventHeader::X)
1702    // and header_str() work regardless of the casing used at insertion.
1703
1704    #[test]
1705    fn set_header_normalizes_known_enum_variant() {
1706        let mut event = EslEvent::new();
1707        event.set_header("unique-id", "abc-123");
1708        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1709    }
1710
1711    #[test]
1712    fn set_header_normalizes_codec_header() {
1713        let mut event = EslEvent::new();
1714        event.set_header("channel-read-codec-bit-rate", "128000");
1715        assert_eq!(
1716            event.header(EventHeader::ChannelReadCodecBitRate),
1717            Some("128000")
1718        );
1719    }
1720
1721    #[test]
1722    fn header_str_finds_by_original_key() {
1723        let mut event = EslEvent::new();
1724        event.set_header("unique-id", "abc-123");
1725        // Lookup by original non-canonical key should still work
1726        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1727        // Lookup by canonical key also works
1728        assert_eq!(event.header_str("Unique-ID"), Some("abc-123"));
1729    }
1730
1731    #[test]
1732    fn header_str_finds_unknown_dash_header_by_original() {
1733        let mut event = EslEvent::new();
1734        event.set_header("x-custom-header", "val");
1735        // Stored as Title-Case
1736        assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
1737        // Original key also works via alias
1738        assert_eq!(event.header_str("x-custom-header"), Some("val"));
1739    }
1740
1741    #[test]
1742    fn set_header_underscore_passthrough_preserves_sip_h() {
1743        let mut event = EslEvent::new();
1744        event.set_header("variable_sip_h_X-My-CUSTOM-Header", "val");
1745        assert_eq!(
1746            event.header_str("variable_sip_h_X-My-CUSTOM-Header"),
1747            Some("val")
1748        );
1749    }
1750
1751    #[test]
1752    fn set_header_different_casing_overwrites() {
1753        let mut event = EslEvent::new();
1754        event.set_header("Unique-ID", "first");
1755        event.set_header("unique-id", "second");
1756        // Both normalize to "Unique-ID", second overwrites first
1757        assert_eq!(event.header(EventHeader::UniqueId), Some("second"));
1758    }
1759
1760    #[test]
1761    fn remove_header_by_original_key() {
1762        let mut event = EslEvent::new();
1763        event.set_header("unique-id", "abc-123");
1764        let removed = event.remove_header("unique-id");
1765        assert_eq!(removed, Some("abc-123".to_string()));
1766        assert_eq!(event.header(EventHeader::UniqueId), None);
1767    }
1768
1769    #[test]
1770    fn remove_header_by_canonical_key() {
1771        let mut event = EslEvent::new();
1772        event.set_header("unique-id", "abc-123");
1773        let removed = event.remove_header("Unique-ID");
1774        assert_eq!(removed, Some("abc-123".to_string()));
1775        assert_eq!(event.header_str("unique-id"), None);
1776    }
1777
1778    #[test]
1779    fn serde_round_trip_preserves_canonical_lookups() {
1780        let mut event = EslEvent::new();
1781        event.set_header("unique-id", "abc-123");
1782        event.set_header("channel-read-codec-bit-rate", "128000");
1783        let json = serde_json::to_string(&event).unwrap();
1784        let deserialized: EslEvent = serde_json::from_str(&json).unwrap();
1785        assert_eq!(deserialized.header(EventHeader::UniqueId), Some("abc-123"));
1786        assert_eq!(
1787            deserialized.header(EventHeader::ChannelReadCodecBitRate),
1788            Some("128000")
1789        );
1790    }
1791
1792    #[test]
1793    fn serde_deserialize_normalizes_external_json() {
1794        let json = r#"{"event_type":null,"headers":{"unique-id":"abc-123","channel-read-codec-bit-rate":"128000"},"body":null}"#;
1795        let event: EslEvent = serde_json::from_str(json).unwrap();
1796        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1797        assert_eq!(
1798            event.header(EventHeader::ChannelReadCodecBitRate),
1799            Some("128000")
1800        );
1801        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1802    }
1803
1804    #[test]
1805    fn test_event_typed_accessors_invalid_values() {
1806        let mut event = EslEvent::new();
1807        event.set_header("Channel-State", "BOGUS");
1808        event.set_header("Channel-State-Number", "999");
1809        event.set_header("Channel-Call-State", "BOGUS");
1810        event.set_header("Answer-State", "bogus");
1811        event.set_header("Call-Direction", "bogus");
1812        assert!(event
1813            .channel_state()
1814            .is_err());
1815        assert!(event
1816            .channel_state_number()
1817            .is_err());
1818        assert!(event
1819            .call_state()
1820            .is_err());
1821        assert!(event
1822            .answer_state()
1823            .is_err());
1824        assert!(event
1825            .call_direction()
1826            .is_err());
1827    }
1828
1829    // --- EventSubscription tests ---
1830
1831    #[test]
1832    fn new_creates_empty() {
1833        let sub = EventSubscription::new(EventFormat::Plain);
1834        assert!(sub.is_empty());
1835        assert!(!sub.is_all());
1836        assert_eq!(sub.format(), EventFormat::Plain);
1837        assert!(sub
1838            .event_types()
1839            .is_empty());
1840        assert!(sub
1841            .custom_subclass_list()
1842            .is_empty());
1843        assert!(sub
1844            .filters()
1845            .is_empty());
1846    }
1847
1848    #[test]
1849    fn all_creates_all() {
1850        let sub = EventSubscription::all(EventFormat::Json);
1851        assert!(sub.is_all());
1852        assert!(!sub.is_empty());
1853        assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
1854    }
1855
1856    #[test]
1857    fn event_string_typed_only() {
1858        let sub = EventSubscription::new(EventFormat::Plain)
1859            .event(EslEventType::ChannelCreate)
1860            .event(EslEventType::ChannelAnswer);
1861        assert_eq!(
1862            sub.to_event_string(),
1863            Some("CHANNEL_CREATE CHANNEL_ANSWER".to_string())
1864        );
1865    }
1866
1867    #[test]
1868    fn event_string_custom_only() {
1869        let sub = EventSubscription::new(EventFormat::Plain)
1870            .custom_subclass("sofia::register")
1871            .unwrap()
1872            .custom_subclass("sofia::unregister")
1873            .unwrap();
1874        assert_eq!(
1875            sub.to_event_string(),
1876            Some("CUSTOM sofia::register sofia::unregister".to_string())
1877        );
1878    }
1879
1880    #[test]
1881    fn event_string_mixed() {
1882        let sub = EventSubscription::new(EventFormat::Plain)
1883            .event(EslEventType::Heartbeat)
1884            .custom_subclass("sofia::register")
1885            .unwrap();
1886        assert_eq!(
1887            sub.to_event_string(),
1888            Some("HEARTBEAT CUSTOM sofia::register".to_string())
1889        );
1890    }
1891
1892    #[test]
1893    fn event_string_custom_not_duplicated() {
1894        let sub = EventSubscription::new(EventFormat::Plain)
1895            .event(EslEventType::Custom)
1896            .custom_subclass("sofia::register")
1897            .unwrap();
1898        // Should not have "CUSTOM" twice
1899        assert_eq!(
1900            sub.to_event_string(),
1901            Some("CUSTOM sofia::register".to_string())
1902        );
1903    }
1904
1905    #[test]
1906    fn event_string_empty_is_none() {
1907        let sub = EventSubscription::new(EventFormat::Plain);
1908        assert_eq!(sub.to_event_string(), None);
1909    }
1910
1911    #[test]
1912    fn filters_preserve_order() {
1913        let sub = EventSubscription::new(EventFormat::Plain)
1914            .filter(EventHeader::CallDirection, "inbound")
1915            .unwrap()
1916            .filter_raw("X-Custom", "value1")
1917            .unwrap()
1918            .filter(EventHeader::ChannelState, "CS_EXECUTE")
1919            .unwrap();
1920        assert_eq!(
1921            sub.filters(),
1922            &[
1923                ("Call-Direction".to_string(), "inbound".to_string()),
1924                ("X-Custom".to_string(), "value1".to_string()),
1925                ("Channel-State".to_string(), "CS_EXECUTE".to_string()),
1926            ]
1927        );
1928    }
1929
1930    #[test]
1931    fn builder_chain() {
1932        let sub = EventSubscription::new(EventFormat::Plain)
1933            .events(EslEventType::CHANNEL_EVENTS)
1934            .event(EslEventType::Heartbeat)
1935            .custom_subclass("sofia::register")
1936            .unwrap()
1937            .filter(EventHeader::CallDirection, "inbound")
1938            .unwrap()
1939            .with_format(EventFormat::Json);
1940
1941        assert_eq!(sub.format(), EventFormat::Json);
1942        assert!(!sub.is_empty());
1943        assert!(!sub.is_all());
1944        assert!(sub
1945            .event_types()
1946            .contains(&EslEventType::ChannelCreate));
1947        assert!(sub
1948            .event_types()
1949            .contains(&EslEventType::Heartbeat));
1950        assert_eq!(sub.custom_subclass_list(), &["sofia::register"]);
1951        assert_eq!(
1952            sub.filters()
1953                .len(),
1954            1
1955        );
1956    }
1957
1958    #[test]
1959    fn serde_round_trip_subscription() {
1960        let sub = EventSubscription::new(EventFormat::Plain)
1961            .event(EslEventType::ChannelCreate)
1962            .event(EslEventType::Heartbeat)
1963            .custom_subclass("sofia::register")
1964            .unwrap()
1965            .filter(EventHeader::CallDirection, "inbound")
1966            .unwrap();
1967
1968        let json = serde_json::to_string(&sub).unwrap();
1969        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
1970        assert_eq!(sub, deserialized);
1971    }
1972
1973    #[test]
1974    fn serde_rejects_invalid_subclass() {
1975        let json =
1976            r#"{"format":"Plain","events":[],"custom_subclasses":["bad subclass"],"filters":[]}"#;
1977        let result: Result<EventSubscription, _> = serde_json::from_str(json);
1978        assert!(result.is_err());
1979        let err = result
1980            .unwrap_err()
1981            .to_string();
1982        assert!(err.contains("space"), "error should mention space: {err}");
1983    }
1984
1985    #[test]
1986    fn serde_rejects_newline_in_filter() {
1987        let json = r#"{"format":"Plain","events":[],"custom_subclasses":[],"filters":[["Header","val\n"]]}"#;
1988        let result: Result<EventSubscription, _> = serde_json::from_str(json);
1989        assert!(result.is_err());
1990        let err = result
1991            .unwrap_err()
1992            .to_string();
1993        assert!(
1994            err.contains("newline"),
1995            "error should mention newline: {err}"
1996        );
1997    }
1998
1999    #[test]
2000    fn custom_subclass_rejects_space() {
2001        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad subclass");
2002        assert!(result.is_err());
2003    }
2004
2005    #[test]
2006    fn custom_subclass_rejects_newline() {
2007        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad\nsubclass");
2008        assert!(result.is_err());
2009    }
2010
2011    #[test]
2012    fn custom_subclass_rejects_empty() {
2013        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("");
2014        assert!(result.is_err());
2015    }
2016
2017    #[test]
2018    fn filter_raw_rejects_newline_in_header() {
2019        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Bad\nHeader", "value");
2020        assert!(result.is_err());
2021    }
2022
2023    #[test]
2024    fn filter_raw_rejects_newline_in_value() {
2025        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Header", "bad\nvalue");
2026        assert!(result.is_err());
2027    }
2028
2029    #[test]
2030    fn filter_typed_rejects_newline_in_value() {
2031        let result = EventSubscription::new(EventFormat::Plain)
2032            .filter(EventHeader::CallDirection, "bad\nvalue");
2033        assert!(result.is_err());
2034    }
2035
2036    #[test]
2037    fn sofia_event_single() {
2038        let sub =
2039            EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
2040        assert_eq!(
2041            sub.to_event_string(),
2042            Some("CUSTOM sofia::register".to_string())
2043        );
2044    }
2045
2046    #[test]
2047    fn sofia_events_group() {
2048        let sub = EventSubscription::new(EventFormat::Plain)
2049            .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
2050        let event_str = sub
2051            .to_event_string()
2052            .unwrap();
2053        assert!(event_str.starts_with("CUSTOM"));
2054        assert!(event_str.contains("sofia::gateway_state"));
2055        assert!(event_str.contains("sofia::gateway_add"));
2056        assert!(event_str.contains("sofia::gateway_delete"));
2057        assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
2058    }
2059
2060    #[test]
2061    fn sofia_event_mixed_with_typed_events() {
2062        let sub = EventSubscription::new(EventFormat::Plain)
2063            .event(EslEventType::Heartbeat)
2064            .sofia_event(SofiaEventSubclass::GatewayState);
2065        assert_eq!(
2066            sub.to_event_string(),
2067            Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
2068        );
2069    }
2070}