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    raw_events: Vec<String>,
446    custom_subclasses: Vec<String>,
447    filters: Vec<(String, String)>,
448}
449
450/// Validates that a raw event name is safe for ESL wire use.
451///
452/// Rejects empty strings, spaces (would split into multiple event names on
453/// the wire), and newline/carriage return injection.
454fn validate_raw_event(s: &str) -> Result<(), EventSubscriptionError> {
455    if s.is_empty() {
456        return Err(EventSubscriptionError("raw event cannot be empty".into()));
457    }
458    if s.contains('\n') || s.contains('\r') {
459        return Err(EventSubscriptionError(format!(
460            "raw event contains newline: {:?}",
461            s
462        )));
463    }
464    if s.contains(' ') {
465        return Err(EventSubscriptionError(format!(
466            "raw event contains space: {:?}",
467            s
468        )));
469    }
470    Ok(())
471}
472
473/// Validates that a custom subclass token is safe for ESL wire use.
474fn validate_custom_subclass(s: &str) -> Result<(), EventSubscriptionError> {
475    if s.is_empty() {
476        return Err(EventSubscriptionError(
477            "custom subclass cannot be empty".into(),
478        ));
479    }
480    if s.contains('\n') || s.contains('\r') {
481        return Err(EventSubscriptionError(format!(
482            "custom subclass contains newline: {:?}",
483            s
484        )));
485    }
486    if s.contains(' ') {
487        return Err(EventSubscriptionError(format!(
488            "custom subclass contains space: {:?}",
489            s
490        )));
491    }
492    Ok(())
493}
494
495/// Validates that a filter header or value has no newline characters.
496fn validate_filter_field(field: &str, label: &str) -> Result<(), EventSubscriptionError> {
497    if field.contains('\n') || field.contains('\r') {
498        return Err(EventSubscriptionError(format!(
499            "filter {} contains newline: {:?}",
500            label, field
501        )));
502    }
503    Ok(())
504}
505
506impl EventSubscription {
507    /// Create an empty subscription with the given format.
508    pub fn new(format: EventFormat) -> Self {
509        Self {
510            format,
511            events: Vec::new(),
512            raw_events: Vec::new(),
513            custom_subclasses: Vec::new(),
514            filters: Vec::new(),
515        }
516    }
517
518    /// Create a subscription for all events.
519    pub fn all(format: EventFormat) -> Self {
520        Self {
521            format,
522            events: vec![EslEventType::All],
523            raw_events: Vec::new(),
524            custom_subclasses: Vec::new(),
525            filters: Vec::new(),
526        }
527    }
528
529    /// Add a single event type.
530    pub fn event(mut self, event: EslEventType) -> Self {
531        self.events
532            .push(event);
533        self
534    }
535
536    /// Add multiple event types (e.g. from group constants like `EslEventType::CHANNEL_EVENTS`).
537    pub fn events<T: IntoIterator<Item = impl std::borrow::Borrow<EslEventType>>>(
538        mut self,
539        events: T,
540    ) -> Self {
541        self.events
542            .extend(
543                events
544                    .into_iter()
545                    .map(|e| *e.borrow()),
546            );
547        self
548    }
549
550    /// Add a single event by wire name.
551    ///
552    /// Escape hatch for events the [`EslEventType`] enum hasn't yet been
553    /// updated to cover. The argument is validated for newline injection,
554    /// spaces, and emptiness.
555    ///
556    /// Raw events appear on the wire alongside typed events when
557    /// [`to_event_string()`](Self::to_event_string) is called.
558    pub fn event_raw(mut self, event: impl Into<String>) -> Result<Self, EventSubscriptionError> {
559        let s = event.into();
560        validate_raw_event(&s)?;
561        self.raw_events
562            .push(s);
563        Ok(self)
564    }
565
566    /// Add multiple events by wire name.
567    ///
568    /// Returns `Err` on the first invalid entry.
569    pub fn events_raw<I, S>(mut self, events: I) -> Result<Self, EventSubscriptionError>
570    where
571        I: IntoIterator<Item = S>,
572        S: Into<String>,
573    {
574        for e in events {
575            let s = e.into();
576            validate_raw_event(&s)?;
577            self.raw_events
578                .push(s);
579        }
580        Ok(self)
581    }
582
583    /// Add a custom subclass (e.g. `"sofia::register"`).
584    ///
585    /// Returns `Err` if the subclass contains spaces, newlines, or is empty.
586    pub fn custom_subclass(
587        mut self,
588        subclass: impl Into<String>,
589    ) -> Result<Self, EventSubscriptionError> {
590        let s = subclass.into();
591        validate_custom_subclass(&s)?;
592        self.custom_subclasses
593            .push(s);
594        Ok(self)
595    }
596
597    /// Add multiple custom subclasses.
598    ///
599    /// Returns `Err` on the first invalid subclass.
600    pub fn custom_subclasses(
601        mut self,
602        subclasses: impl IntoIterator<Item = impl Into<String>>,
603    ) -> Result<Self, EventSubscriptionError> {
604        for s in subclasses {
605            let s = s.into();
606            validate_custom_subclass(&s)?;
607            self.custom_subclasses
608                .push(s);
609        }
610        Ok(self)
611    }
612
613    /// Subscribe to a single Sofia event subclass.
614    ///
615    /// Convenience wrapper around [`custom_subclass()`](Self::custom_subclass) that
616    /// accepts a typed [`SofiaEventSubclass`] instead of a raw string.
617    pub fn sofia_event(mut self, subclass: SofiaEventSubclass) -> Self {
618        self.custom_subclasses
619            .push(
620                subclass
621                    .as_str()
622                    .to_string(),
623            );
624        self
625    }
626
627    /// Subscribe to multiple Sofia event subclasses.
628    pub fn sofia_events(
629        mut self,
630        subclasses: impl IntoIterator<Item = impl std::borrow::Borrow<SofiaEventSubclass>>,
631    ) -> Self {
632        self.custom_subclasses
633            .extend(
634                subclasses
635                    .into_iter()
636                    .map(|s| {
637                        s.borrow()
638                            .as_str()
639                            .to_string()
640                    }),
641            );
642        self
643    }
644
645    /// Add a filter with a typed header.
646    ///
647    /// The header enum is always valid; only the value is validated.
648    pub fn filter(
649        self,
650        header: crate::headers::EventHeader,
651        value: impl Into<String>,
652    ) -> Result<Self, EventSubscriptionError> {
653        let v = value.into();
654        validate_filter_field(&v, "value")?;
655        let mut s = self;
656        s.filters
657            .push((
658                header
659                    .as_str()
660                    .to_string(),
661                v,
662            ));
663        Ok(s)
664    }
665
666    /// Add a filter with raw header and value strings.
667    ///
668    /// Both header and value are validated against newline injection.
669    pub fn filter_raw(
670        self,
671        header: impl Into<String>,
672        value: impl Into<String>,
673    ) -> Result<Self, EventSubscriptionError> {
674        let h = header.into();
675        let v = value.into();
676        validate_filter_field(&h, "header")?;
677        validate_filter_field(&v, "value")?;
678        let mut s = self;
679        s.filters
680            .push((h, v));
681        Ok(s)
682    }
683
684    /// Change the event format.
685    pub fn with_format(mut self, format: EventFormat) -> Self {
686        self.format = format;
687        self
688    }
689
690    /// The event format.
691    pub fn format(&self) -> EventFormat {
692        self.format
693    }
694
695    /// Mutable reference to the event format.
696    pub fn format_mut(&mut self) -> &mut EventFormat {
697        &mut self.format
698    }
699
700    /// The subscribed event types.
701    pub fn event_types(&self) -> &[EslEventType] {
702        &self.events
703    }
704
705    /// Mutable access to the event types list.
706    pub fn event_types_mut(&mut self) -> &mut Vec<EslEventType> {
707        &mut self.events
708    }
709
710    /// Events subscribed by raw wire name (see [`event_raw`](Self::event_raw)).
711    pub fn event_types_raw(&self) -> &[String] {
712        &self.raw_events
713    }
714
715    /// Mutable access to the raw event list.
716    ///
717    /// Direct push to this list bypasses [`event_raw`](Self::event_raw)'s
718    /// validation. Callers are responsible for ensuring entries contain no
719    /// newlines, spaces, or empty strings.
720    pub fn event_types_raw_mut(&mut self) -> &mut Vec<String> {
721        &mut self.raw_events
722    }
723
724    /// The subscribed custom subclasses.
725    pub fn custom_subclass_list(&self) -> &[String] {
726        &self.custom_subclasses
727    }
728
729    /// Mutable access to the custom subclasses list.
730    pub fn custom_subclasses_mut(&mut self) -> &mut Vec<String> {
731        &mut self.custom_subclasses
732    }
733
734    /// The event filters as (header, value) pairs.
735    pub fn filters(&self) -> &[(String, String)] {
736        &self.filters
737    }
738
739    /// Mutable access to the filters list.
740    pub fn filters_mut(&mut self) -> &mut Vec<(String, String)> {
741        &mut self.filters
742    }
743
744    /// Whether the subscription includes all events.
745    pub fn is_all(&self) -> bool {
746        self.events
747            .contains(&EslEventType::All)
748    }
749
750    /// Whether the subscription has no events, no raw events, and no
751    /// custom subclasses.
752    pub fn is_empty(&self) -> bool {
753        self.events
754            .is_empty()
755            && self
756                .raw_events
757                .is_empty()
758            && self
759                .custom_subclasses
760                .is_empty()
761    }
762
763    /// Build the event string for the ESL `event` command.
764    ///
765    /// Returns `None` if no events, raw events, or custom subclasses are
766    /// configured. Returns `Some("ALL")` if `EslEventType::All` is present.
767    /// Otherwise returns space-separated typed event names, then raw event
768    /// names, with custom subclasses appended after a `CUSTOM` token.
769    pub fn to_event_string(&self) -> Option<String> {
770        if self
771            .events
772            .contains(&EslEventType::All)
773        {
774            return Some("ALL".to_string());
775        }
776
777        let mut parts: Vec<&str> = self
778            .events
779            .iter()
780            .map(|e| e.as_str())
781            .collect();
782
783        parts.extend(
784            self.raw_events
785                .iter()
786                .map(|s| s.as_str()),
787        );
788
789        if !self
790            .custom_subclasses
791            .is_empty()
792        {
793            if !self
794                .events
795                .contains(&EslEventType::Custom)
796            {
797                parts.push("CUSTOM");
798            }
799            for sc in &self.custom_subclasses {
800                parts.push(sc.as_str());
801            }
802        }
803
804        if parts.is_empty() {
805            None
806        } else {
807            Some(parts.join(" "))
808        }
809    }
810}
811
812#[cfg(feature = "serde")]
813mod event_subscription_serde {
814    use super::*;
815    use serde::{Deserialize, Serialize};
816
817    #[derive(Serialize, Deserialize)]
818    struct EventSubscriptionRaw {
819        format: EventFormat,
820        #[serde(default)]
821        events: Vec<EslEventType>,
822        #[serde(default, skip_serializing_if = "Vec::is_empty")]
823        raw_events: Vec<String>,
824        #[serde(default)]
825        custom_subclasses: Vec<String>,
826        #[serde(default)]
827        filters: Vec<(String, String)>,
828    }
829
830    impl TryFrom<EventSubscriptionRaw> for EventSubscription {
831        type Error = EventSubscriptionError;
832
833        fn try_from(raw: EventSubscriptionRaw) -> Result<Self, Self::Error> {
834            for re in &raw.raw_events {
835                validate_raw_event(re)?;
836            }
837            for sc in &raw.custom_subclasses {
838                validate_custom_subclass(sc)?;
839            }
840            for (h, v) in &raw.filters {
841                validate_filter_field(h, "header")?;
842                validate_filter_field(v, "value")?;
843            }
844            Ok(EventSubscription {
845                format: raw.format,
846                events: raw.events,
847                raw_events: raw.raw_events,
848                custom_subclasses: raw.custom_subclasses,
849                filters: raw.filters,
850            })
851        }
852    }
853
854    impl Serialize for EventSubscription {
855        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
856            let raw = EventSubscriptionRaw {
857                format: self.format,
858                events: self
859                    .events
860                    .clone(),
861                raw_events: self
862                    .raw_events
863                    .clone(),
864                custom_subclasses: self
865                    .custom_subclasses
866                    .clone(),
867                filters: self
868                    .filters
869                    .clone(),
870            };
871            raw.serialize(serializer)
872        }
873    }
874
875    impl<'de> Deserialize<'de> for EventSubscription {
876        fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
877            let raw = EventSubscriptionRaw::deserialize(deserializer)?;
878            EventSubscription::try_from(raw).map_err(serde::de::Error::custom)
879        }
880    }
881}
882
883/// Event priority levels matching FreeSWITCH `esl_priority_t`
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
885#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
886#[non_exhaustive]
887pub enum EslEventPriority {
888    /// Default priority.
889    Normal,
890    /// Lower than normal.
891    Low,
892    /// Higher than normal.
893    High,
894}
895
896impl fmt::Display for EslEventPriority {
897    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
898        match self {
899            EslEventPriority::Normal => write!(f, "NORMAL"),
900            EslEventPriority::Low => write!(f, "LOW"),
901            EslEventPriority::High => write!(f, "HIGH"),
902        }
903    }
904}
905
906/// Error returned when parsing an invalid priority string.
907#[derive(Debug, Clone, PartialEq, Eq)]
908pub struct ParsePriorityError(pub String);
909
910impl fmt::Display for ParsePriorityError {
911    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
912        write!(f, "unknown priority: {}", self.0)
913    }
914}
915
916impl std::error::Error for ParsePriorityError {}
917
918impl FromStr for EslEventPriority {
919    type Err = ParsePriorityError;
920
921    fn from_str(s: &str) -> Result<Self, Self::Err> {
922        match s {
923            "NORMAL" => Ok(EslEventPriority::Normal),
924            "LOW" => Ok(EslEventPriority::Low),
925            "HIGH" => Ok(EslEventPriority::High),
926            _ => Err(ParsePriorityError(s.to_string())),
927        }
928    }
929}
930
931/// ESL Event structure containing headers and optional body
932#[derive(Debug, Clone, Eq)]
933#[cfg_attr(feature = "serde", derive(serde::Serialize))]
934pub struct EslEvent {
935    event_type: Option<EslEventType>,
936    headers: IndexMap<String, String>,
937    /// Alias map from original wire key to normalized key, populated only
938    /// when the original differs from its normalized form (mixed-case
939    /// CODEC events, variant-cased log headers). Lets `header_str` resolve
940    /// non-canonical casing without allocating on every lookup.
941    ///
942    /// Derived from `headers`. Marked `#[serde(skip)]` — serde round trips
943    /// through `set_header()` during deserialization, which rebuilds this
944    /// map from the canonical keys. See the `Deserialize` impl below and
945    /// the `original_keys_rebuilt_after_serde_roundtrip` test.
946    #[cfg_attr(feature = "serde", serde(skip))]
947    original_keys: IndexMap<String, String>,
948    body: Option<String>,
949}
950
951impl EslEvent {
952    /// Create a new empty event
953    pub fn new() -> Self {
954        Self {
955            event_type: None,
956            headers: IndexMap::new(),
957            original_keys: IndexMap::new(),
958            body: None,
959        }
960    }
961
962    /// Create event with specified type
963    pub fn with_type(event_type: EslEventType) -> Self {
964        Self {
965            event_type: Some(event_type),
966            headers: IndexMap::new(),
967            original_keys: IndexMap::new(),
968            body: None,
969        }
970    }
971
972    /// Parsed event type, if recognized.
973    pub fn event_type(&self) -> Option<EslEventType> {
974        self.event_type
975    }
976
977    /// Override the event type.
978    pub fn set_event_type(&mut self, event_type: Option<EslEventType>) {
979        self.event_type = event_type;
980    }
981
982    /// Look up a header by its [`EventHeader`] enum variant (case-sensitive).
983    ///
984    /// For headers not covered by `EventHeader`, use [`header_str()`](Self::header_str).
985    pub fn header(&self, name: EventHeader) -> Option<&str> {
986        self.headers
987            .get(name.as_str())
988            .map(|s| s.as_str())
989    }
990
991    /// Look up a header by name, trying the canonical key first then falling
992    /// back through the alias map for non-canonical lookups.
993    ///
994    /// Use [`header()`](Self::header) with an [`EventHeader`] variant for known
995    /// headers. This method is for headers not (yet) covered by the enum,
996    /// such as custom `X-` headers or FreeSWITCH headers added after this
997    /// library was published.
998    pub fn header_str(&self, name: &str) -> Option<&str> {
999        self.headers
1000            .get(name)
1001            .or_else(|| {
1002                self.original_keys
1003                    .get(name)
1004                    .and_then(|normalized| {
1005                        self.headers
1006                            .get(normalized)
1007                    })
1008            })
1009            .map(|s| s.as_str())
1010    }
1011
1012    /// Look up a channel variable by its bare name.
1013    ///
1014    /// Equivalent to [`variable()`](Self::variable) but matches the
1015    /// [`HeaderLookup`] trait signature.
1016    pub fn variable_str(&self, name: &str) -> Option<&str> {
1017        let key = format!("variable_{}", name);
1018        self.header_str(&key)
1019    }
1020
1021    /// All headers as a map.
1022    pub fn headers(&self) -> &IndexMap<String, String> {
1023        &self.headers
1024    }
1025
1026    /// Set or overwrite a header, normalizing the key.
1027    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
1028        let original = name.into();
1029        let normalized = normalize_header_key(&original);
1030        if original != normalized {
1031            self.original_keys
1032                .insert(original, normalized.clone());
1033        }
1034        self.headers
1035            .insert(normalized, value.into());
1036    }
1037
1038    /// Remove a header, returning its value if it existed.
1039    ///
1040    /// Accepts both canonical and original (non-normalized) key names.
1041    pub fn remove_header(&mut self, name: impl AsRef<str>) -> Option<String> {
1042        let name = name.as_ref();
1043        if let Some(value) = self
1044            .headers
1045            .shift_remove(name)
1046        {
1047            return Some(value);
1048        }
1049        if let Some(normalized) = self
1050            .original_keys
1051            .shift_remove(name)
1052        {
1053            return self
1054                .headers
1055                .shift_remove(&normalized);
1056        }
1057        None
1058    }
1059
1060    /// Event body (the content after the blank line in plain-text events).
1061    pub fn body(&self) -> Option<&str> {
1062        self.body
1063            .as_deref()
1064    }
1065
1066    /// Set the event body.
1067    pub fn set_body(&mut self, body: impl Into<String>) {
1068        self.body = Some(body.into());
1069    }
1070
1071    /// Sets the `priority` header carried on the event.
1072    ///
1073    /// FreeSWITCH stores this as metadata but does **not** use it for dispatch
1074    /// ordering -- all events are delivered FIFO regardless of priority.
1075    pub fn set_priority(&mut self, priority: EslEventPriority) {
1076        self.set_header(EventHeader::Priority.as_str(), priority.to_string());
1077    }
1078
1079    /// Append a value to a multi-value header (PUSH semantics).
1080    ///
1081    /// If the header doesn't exist, sets it as a plain value.
1082    /// If it exists as a plain value, converts to `ARRAY::old|:new`.
1083    /// If it already has an `ARRAY::` prefix, appends the new value.
1084    ///
1085    /// Returns [`EslArrayError::TooManyItems`] if the existing header already
1086    /// contains [`MAX_ARRAY_ITEMS`](crate::MAX_ARRAY_ITEMS) items.
1087    ///
1088    /// ```
1089    /// # use freeswitch_types::EslEvent;
1090    /// let mut event = EslEvent::new();
1091    /// event.push_header("X-Test", "first").unwrap();
1092    /// event.push_header("X-Test", "second").unwrap();
1093    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
1094    /// ```
1095    pub fn push_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
1096        self.stack_header(name, value, EslArray::push)
1097    }
1098
1099    /// Prepend a value to a multi-value header (UNSHIFT semantics).
1100    ///
1101    /// Same conversion rules as [`push_header()`](Self::push_header), but
1102    /// inserts at the front.
1103    ///
1104    /// ```
1105    /// # use freeswitch_types::EslEvent;
1106    /// let mut event = EslEvent::new();
1107    /// event.set_header("X-Test", "ARRAY::b|:c");
1108    /// event.unshift_header("X-Test", "a").unwrap();
1109    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1110    /// ```
1111    pub fn unshift_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
1112        self.stack_header(name, value, EslArray::unshift)
1113    }
1114
1115    fn stack_header(
1116        &mut self,
1117        name: &str,
1118        value: &str,
1119        op: fn(&mut EslArray, String),
1120    ) -> Result<(), EslArrayError> {
1121        match self
1122            .headers
1123            .get(name)
1124        {
1125            None => {
1126                self.set_header(name, value);
1127            }
1128            Some(existing) => {
1129                let arr = match EslArray::parse(existing) {
1130                    Ok(arr) => arr,
1131                    Err(EslArrayError::MissingPrefix) => EslArray::new(vec![existing.clone()]),
1132                    Err(e) => return Err(e),
1133                };
1134                if arr.len() >= crate::variables::MAX_ARRAY_ITEMS {
1135                    return Err(EslArrayError::TooManyItems {
1136                        count: arr.len(),
1137                        max: crate::variables::MAX_ARRAY_ITEMS,
1138                    });
1139                }
1140                let mut arr = arr;
1141                op(&mut arr, value.into());
1142                self.set_header(name, arr.to_string());
1143            }
1144        }
1145        Ok(())
1146    }
1147
1148    /// Check whether this event matches the given type.
1149    pub fn is_event_type(&self, event_type: EslEventType) -> bool {
1150        self.event_type == Some(event_type)
1151    }
1152
1153    /// Serialize to ESL plain text wire format with percent-encoded header values.
1154    ///
1155    /// This is the inverse of `EslParser::parse_plain_event()`. The output can
1156    /// be fed back through the parser to reconstruct an equivalent `EslEvent`
1157    /// (round-trip).
1158    ///
1159    /// Headers are emitted in insertion order (which matches wire order when the
1160    /// event was parsed from the network). `Content-Length` from stored headers
1161    /// is skipped and recomputed from the body if present.
1162    pub fn to_plain_format(&self) -> String {
1163        use std::fmt::Write;
1164        let mut result = String::new();
1165
1166        for (key, value) in &self.headers {
1167            if key == "Content-Length" {
1168                continue;
1169            }
1170            let _ = writeln!(
1171                result,
1172                "{}: {}",
1173                key,
1174                percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
1175            );
1176        }
1177
1178        if let Some(body) = &self.body {
1179            let _ = writeln!(result, "Content-Length: {}", body.len());
1180            result.push('\n');
1181            result.push_str(body);
1182        } else {
1183            result.push('\n');
1184        }
1185
1186        result
1187    }
1188}
1189
1190impl Default for EslEvent {
1191    fn default() -> Self {
1192        Self::new()
1193    }
1194}
1195
1196impl HeaderLookup for EslEvent {
1197    fn header_str(&self, name: &str) -> Option<&str> {
1198        EslEvent::header_str(self, name)
1199    }
1200
1201    fn variable_str(&self, name: &str) -> Option<&str> {
1202        let key = format!("variable_{}", name);
1203        self.header_str(&key)
1204    }
1205}
1206
1207impl sip_header::SipHeaderLookup for EslEvent {
1208    fn sip_header_str(&self, name: &str) -> Option<&str> {
1209        EslEvent::header_str(self, name)
1210    }
1211}
1212
1213impl PartialEq for EslEvent {
1214    fn eq(&self, other: &Self) -> bool {
1215        self.event_type == other.event_type
1216            && self.headers == other.headers
1217            && self.body == other.body
1218    }
1219}
1220
1221impl std::hash::Hash for EslEvent {
1222    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1223        self.event_type
1224            .hash(state);
1225        for (k, v) in &self.headers {
1226            k.hash(state);
1227            v.hash(state);
1228        }
1229        self.body
1230            .hash(state);
1231    }
1232}
1233
1234#[cfg(feature = "serde")]
1235impl<'de> serde::Deserialize<'de> for EslEvent {
1236    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1237    where
1238        D: serde::Deserializer<'de>,
1239    {
1240        #[derive(serde::Deserialize)]
1241        struct Raw {
1242            event_type: Option<EslEventType>,
1243            headers: IndexMap<String, String>,
1244            body: Option<String>,
1245        }
1246        let raw = Raw::deserialize(deserializer)?;
1247        let mut event = EslEvent::new();
1248        event.event_type = raw.event_type;
1249        event.body = raw.body;
1250        for (k, v) in raw.headers {
1251            event.set_header(k, v);
1252        }
1253        Ok(event)
1254    }
1255}
1256
1257#[cfg(test)]
1258mod tests {
1259    use super::*;
1260
1261    #[test]
1262    fn headers_preserve_insertion_order() {
1263        let mut event = EslEvent::new();
1264        event.set_header("Zebra", "last");
1265        event.set_header("Alpha", "first");
1266        event.set_header("Middle", "mid");
1267        let keys: Vec<&str> = event
1268            .headers()
1269            .keys()
1270            .map(|s| s.as_str())
1271            .collect();
1272        assert_eq!(keys, vec!["Zebra", "Alpha", "Middle"]);
1273    }
1274
1275    #[test]
1276    fn test_notify_in_parse() {
1277        assert_eq!(
1278            EslEventType::parse_event_type("NOTIFY_IN"),
1279            Some(EslEventType::NotifyIn)
1280        );
1281        assert_eq!(EslEventType::parse_event_type("notify_in"), None);
1282    }
1283
1284    #[test]
1285    fn test_notify_in_display() {
1286        assert_eq!(EslEventType::NotifyIn.to_string(), "NOTIFY_IN");
1287    }
1288
1289    #[test]
1290    fn test_notify_in_distinct_from_notify() {
1291        assert_ne!(EslEventType::Notify, EslEventType::NotifyIn);
1292        assert_ne!(
1293            EslEventType::Notify.to_string(),
1294            EslEventType::NotifyIn.to_string()
1295        );
1296    }
1297
1298    #[test]
1299    fn test_wire_names_match_c_esl() {
1300        assert_eq!(
1301            EslEventType::ChannelOutgoing.to_string(),
1302            "CHANNEL_OUTGOING"
1303        );
1304        assert_eq!(EslEventType::Api.to_string(), "API");
1305        assert_eq!(EslEventType::ReloadXml.to_string(), "RELOADXML");
1306        assert_eq!(EslEventType::PresenceIn.to_string(), "PRESENCE_IN");
1307        assert_eq!(EslEventType::Roster.to_string(), "ROSTER");
1308        assert_eq!(EslEventType::Text.to_string(), "TEXT");
1309        assert_eq!(EslEventType::ReSchedule.to_string(), "RE_SCHEDULE");
1310
1311        assert_eq!(
1312            EslEventType::parse_event_type("CHANNEL_OUTGOING"),
1313            Some(EslEventType::ChannelOutgoing)
1314        );
1315        assert_eq!(
1316            EslEventType::parse_event_type("API"),
1317            Some(EslEventType::Api)
1318        );
1319        assert_eq!(
1320            EslEventType::parse_event_type("RELOADXML"),
1321            Some(EslEventType::ReloadXml)
1322        );
1323        assert_eq!(
1324            EslEventType::parse_event_type("PRESENCE_IN"),
1325            Some(EslEventType::PresenceIn)
1326        );
1327    }
1328
1329    #[test]
1330    fn test_event_type_from_str() {
1331        assert_eq!(
1332            "CHANNEL_ANSWER".parse::<EslEventType>(),
1333            Ok(EslEventType::ChannelAnswer)
1334        );
1335        assert!("channel_answer"
1336            .parse::<EslEventType>()
1337            .is_err());
1338        assert!("UNKNOWN_EVENT"
1339            .parse::<EslEventType>()
1340            .is_err());
1341    }
1342
1343    #[test]
1344    fn test_remove_header() {
1345        let mut event = EslEvent::new();
1346        event.set_header("Foo", "bar");
1347        event.set_header("Baz", "qux");
1348
1349        let removed = event.remove_header("Foo");
1350        assert_eq!(removed, Some("bar".to_string()));
1351        assert!(event
1352            .header_str("Foo")
1353            .is_none());
1354        assert_eq!(event.header_str("Baz"), Some("qux"));
1355
1356        let removed_again = event.remove_header("Foo");
1357        assert_eq!(removed_again, None);
1358    }
1359
1360    #[test]
1361    fn test_to_plain_format_basic() {
1362        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1363        event.set_header("Event-Name", "HEARTBEAT");
1364        event.set_header("Core-UUID", "abc-123");
1365
1366        let plain = event.to_plain_format();
1367
1368        assert!(plain.starts_with("Event-Name: "));
1369        assert!(plain.contains("Core-UUID: "));
1370        assert!(plain.ends_with("\n\n"));
1371    }
1372
1373    #[test]
1374    fn test_to_plain_format_percent_encoding() {
1375        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1376        event.set_header("Event-Name", "HEARTBEAT");
1377        event.set_header("Up-Time", "0 years, 0 days");
1378
1379        let plain = event.to_plain_format();
1380
1381        assert!(!plain.contains("0 years, 0 days"));
1382        assert!(plain.contains("Up-Time: "));
1383        assert!(plain.contains("%20"));
1384    }
1385
1386    #[test]
1387    fn test_to_plain_format_with_body() {
1388        let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
1389        event.set_header("Event-Name", "BACKGROUND_JOB");
1390        event.set_header("Job-UUID", "def-456");
1391        event.set_body("+OK result\n".to_string());
1392
1393        let plain = event.to_plain_format();
1394
1395        assert!(plain.contains("Content-Length: 11\n"));
1396        assert!(plain.ends_with("\n\n+OK result\n"));
1397    }
1398
1399    #[test]
1400    fn test_to_plain_format_preserves_insertion_order() {
1401        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
1402        event.set_header("Event-Name", "HEARTBEAT");
1403        event.set_header("Core-UUID", "abc-123");
1404        event.set_header("FreeSWITCH-Hostname", "fs01");
1405        event.set_header("Up-Time", "0 years, 1 day");
1406
1407        let plain = event.to_plain_format();
1408        let lines: Vec<&str> = plain
1409            .lines()
1410            .collect();
1411        assert!(lines[0].starts_with("Event-Name: "));
1412        assert!(lines[1].starts_with("Core-UUID: "));
1413        assert!(lines[2].starts_with("FreeSWITCH-Hostname: "));
1414        assert!(lines[3].starts_with("Up-Time: "));
1415    }
1416
1417    #[test]
1418    fn test_to_plain_format_round_trip() {
1419        let mut original = EslEvent::with_type(EslEventType::ChannelCreate);
1420        original.set_header("Event-Name", "CHANNEL_CREATE");
1421        original.set_header("Core-UUID", "abc-123");
1422        original.set_header("Channel-Name", "sofia/internal/1000@example.com");
1423        original.set_header("Caller-Caller-ID-Name", "Jérôme Poulin");
1424        original.set_body("some body content");
1425
1426        let plain = original.to_plain_format();
1427
1428        // Simulate what EslParser::parse_plain_event does
1429        let (header_section, inner_body) = if let Some(pos) = plain.find("\n\n") {
1430            (&plain[..pos], Some(&plain[pos + 2..]))
1431        } else {
1432            (plain.as_str(), None)
1433        };
1434
1435        let mut parsed = EslEvent::new();
1436        for line in header_section.lines() {
1437            let line = line.trim();
1438            if line.is_empty() {
1439                continue;
1440            }
1441            if let Some(colon_pos) = line.find(':') {
1442                let key = line[..colon_pos].trim();
1443                if key == "Content-Length" {
1444                    continue;
1445                }
1446                let raw_value = line[colon_pos + 1..].trim();
1447                let value = percent_encoding::percent_decode_str(raw_value)
1448                    .decode_utf8()
1449                    .unwrap()
1450                    .into_owned();
1451                parsed.set_header(key, value);
1452            }
1453        }
1454        if let Some(ib) = inner_body {
1455            if !ib.is_empty() {
1456                parsed.set_body(ib);
1457            }
1458        }
1459
1460        assert_eq!(original.headers(), parsed.headers());
1461        assert_eq!(original.body(), parsed.body());
1462    }
1463
1464    #[test]
1465    fn test_set_priority_normal() {
1466        let mut event = EslEvent::new();
1467        event.set_priority(EslEventPriority::Normal);
1468        assert_eq!(
1469            event
1470                .priority()
1471                .unwrap(),
1472            Some(EslEventPriority::Normal)
1473        );
1474        assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
1475    }
1476
1477    #[test]
1478    fn test_set_priority_high() {
1479        let mut event = EslEvent::new();
1480        event.set_priority(EslEventPriority::High);
1481        assert_eq!(
1482            event
1483                .priority()
1484                .unwrap(),
1485            Some(EslEventPriority::High)
1486        );
1487        assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
1488    }
1489
1490    #[test]
1491    fn test_priority_display() {
1492        assert_eq!(EslEventPriority::Normal.to_string(), "NORMAL");
1493        assert_eq!(EslEventPriority::Low.to_string(), "LOW");
1494        assert_eq!(EslEventPriority::High.to_string(), "HIGH");
1495    }
1496
1497    #[test]
1498    fn test_priority_from_str() {
1499        assert_eq!(
1500            "NORMAL".parse::<EslEventPriority>(),
1501            Ok(EslEventPriority::Normal)
1502        );
1503        assert_eq!("LOW".parse::<EslEventPriority>(), Ok(EslEventPriority::Low));
1504        assert_eq!(
1505            "HIGH".parse::<EslEventPriority>(),
1506            Ok(EslEventPriority::High)
1507        );
1508        assert!("INVALID"
1509            .parse::<EslEventPriority>()
1510            .is_err());
1511    }
1512
1513    #[test]
1514    fn test_priority_from_str_rejects_wrong_case() {
1515        assert!("normal"
1516            .parse::<EslEventPriority>()
1517            .is_err());
1518        assert!("Low"
1519            .parse::<EslEventPriority>()
1520            .is_err());
1521        assert!("hIgH"
1522            .parse::<EslEventPriority>()
1523            .is_err());
1524    }
1525
1526    #[test]
1527    fn test_push_header_new() {
1528        let mut event = EslEvent::new();
1529        event
1530            .push_header("X-Test", "first")
1531            .unwrap();
1532        assert_eq!(event.header_str("X-Test"), Some("first"));
1533    }
1534
1535    #[test]
1536    fn test_push_header_existing_plain() {
1537        let mut event = EslEvent::new();
1538        event.set_header("X-Test", "first");
1539        event
1540            .push_header("X-Test", "second")
1541            .unwrap();
1542        assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
1543    }
1544
1545    #[test]
1546    fn test_push_header_existing_array() {
1547        let mut event = EslEvent::new();
1548        event.set_header("X-Test", "ARRAY::a|:b");
1549        event
1550            .push_header("X-Test", "c")
1551            .unwrap();
1552        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1553    }
1554
1555    #[test]
1556    fn test_push_header_at_capacity() {
1557        use crate::variables::MAX_ARRAY_ITEMS;
1558        let mut event = EslEvent::new();
1559        let items: Vec<&str> = (0..MAX_ARRAY_ITEMS)
1560            .map(|_| "x")
1561            .collect();
1562        event.set_header("X-Test", format!("ARRAY::{}", items.join("|:")).as_str());
1563        assert!(matches!(
1564            event.push_header("X-Test", "overflow"),
1565            Err(EslArrayError::TooManyItems { .. })
1566        ));
1567    }
1568
1569    #[test]
1570    fn test_unshift_header_new() {
1571        let mut event = EslEvent::new();
1572        event
1573            .unshift_header("X-Test", "only")
1574            .unwrap();
1575        assert_eq!(event.header_str("X-Test"), Some("only"));
1576    }
1577
1578    #[test]
1579    fn test_unshift_header_existing_array() {
1580        let mut event = EslEvent::new();
1581        event.set_header("X-Test", "ARRAY::b|:c");
1582        event
1583            .unshift_header("X-Test", "a")
1584            .unwrap();
1585        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1586    }
1587
1588    #[test]
1589    fn test_sendevent_with_priority_wire_format() {
1590        let mut event = EslEvent::with_type(EslEventType::Custom);
1591        event.set_header("Event-Name", "CUSTOM");
1592        event.set_header("Event-Subclass", "test::priority");
1593        event.set_priority(EslEventPriority::High);
1594
1595        let plain = event.to_plain_format();
1596        assert!(plain.contains("priority: HIGH\n"));
1597    }
1598
1599    #[test]
1600    fn test_convenience_accessors() {
1601        let mut event = EslEvent::new();
1602        event.set_header("Channel-Name", "sofia/internal/1000@example.com");
1603        event.set_header("Caller-Caller-ID-Number", "1000");
1604        event.set_header("Caller-Caller-ID-Name", "Alice");
1605        event.set_header("Hangup-Cause", "NORMAL_CLEARING");
1606        event.set_header("Event-Subclass", "sofia::register");
1607        event.set_header("variable_sip_from_display", "Bob");
1608
1609        assert_eq!(
1610            event.channel_name(),
1611            Some("sofia/internal/1000@example.com")
1612        );
1613        assert_eq!(event.caller_id_number(), Some("1000"));
1614        assert_eq!(event.caller_id_name(), Some("Alice"));
1615        assert_eq!(
1616            event
1617                .hangup_cause()
1618                .unwrap(),
1619            Some(crate::channel::HangupCause::NormalClearing)
1620        );
1621        assert_eq!(event.event_subclass(), Some("sofia::register"));
1622        assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
1623        assert_eq!(event.variable_str("nonexistent"), None);
1624    }
1625
1626    #[test]
1627    fn test_event_format_from_str() {
1628        assert_eq!("plain".parse::<EventFormat>(), Ok(EventFormat::Plain));
1629        assert_eq!("json".parse::<EventFormat>(), Ok(EventFormat::Json));
1630        assert_eq!("xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1631        assert!("foo"
1632            .parse::<EventFormat>()
1633            .is_err());
1634    }
1635
1636    #[test]
1637    fn test_event_format_from_str_case_insensitive() {
1638        assert_eq!("PLAIN".parse::<EventFormat>(), Ok(EventFormat::Plain));
1639        assert_eq!("Json".parse::<EventFormat>(), Ok(EventFormat::Json));
1640        assert_eq!("XML".parse::<EventFormat>(), Ok(EventFormat::Xml));
1641        assert_eq!("Xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1642    }
1643
1644    #[test]
1645    fn test_event_format_from_content_type() {
1646        assert_eq!(
1647            EventFormat::from_content_type("text/event-json"),
1648            Ok(EventFormat::Json)
1649        );
1650        assert_eq!(
1651            EventFormat::from_content_type("text/event-xml"),
1652            Ok(EventFormat::Xml)
1653        );
1654        assert_eq!(
1655            EventFormat::from_content_type("text/event-plain"),
1656            Ok(EventFormat::Plain)
1657        );
1658        assert!(EventFormat::from_content_type("unknown").is_err());
1659    }
1660
1661    // --- EslEvent accessor tests (via HeaderLookup trait) ---
1662
1663    #[test]
1664    fn test_event_channel_state_accessor() {
1665        use crate::channel::ChannelState;
1666        let mut event = EslEvent::new();
1667        event.set_header("Channel-State", "CS_EXECUTE");
1668        assert_eq!(
1669            event
1670                .channel_state()
1671                .unwrap(),
1672            Some(ChannelState::CsExecute)
1673        );
1674    }
1675
1676    #[test]
1677    fn test_event_channel_state_number_accessor() {
1678        use crate::channel::ChannelState;
1679        let mut event = EslEvent::new();
1680        event.set_header("Channel-State-Number", "4");
1681        assert_eq!(
1682            event
1683                .channel_state_number()
1684                .unwrap(),
1685            Some(ChannelState::CsExecute)
1686        );
1687    }
1688
1689    #[test]
1690    fn test_event_call_state_accessor() {
1691        use crate::channel::CallState;
1692        let mut event = EslEvent::new();
1693        event.set_header("Channel-Call-State", "ACTIVE");
1694        assert_eq!(
1695            event
1696                .call_state()
1697                .unwrap(),
1698            Some(CallState::Active)
1699        );
1700    }
1701
1702    #[test]
1703    fn test_event_answer_state_accessor() {
1704        use crate::channel::AnswerState;
1705        let mut event = EslEvent::new();
1706        event.set_header("Answer-State", "answered");
1707        assert_eq!(
1708            event
1709                .answer_state()
1710                .unwrap(),
1711            Some(AnswerState::Answered)
1712        );
1713    }
1714
1715    #[test]
1716    fn test_event_call_direction_accessor() {
1717        use crate::channel::CallDirection;
1718        let mut event = EslEvent::new();
1719        event.set_header("Call-Direction", "inbound");
1720        assert_eq!(
1721            event
1722                .call_direction()
1723                .unwrap(),
1724            Some(CallDirection::Inbound)
1725        );
1726    }
1727
1728    #[test]
1729    fn test_event_typed_accessors_missing_headers() {
1730        let event = EslEvent::new();
1731        assert_eq!(
1732            event
1733                .channel_state()
1734                .unwrap(),
1735            None
1736        );
1737        assert_eq!(
1738            event
1739                .channel_state_number()
1740                .unwrap(),
1741            None
1742        );
1743        assert_eq!(
1744            event
1745                .call_state()
1746                .unwrap(),
1747            None
1748        );
1749        assert_eq!(
1750            event
1751                .answer_state()
1752                .unwrap(),
1753            None
1754        );
1755        assert_eq!(
1756            event
1757                .call_direction()
1758                .unwrap(),
1759            None
1760        );
1761    }
1762
1763    // --- Repeating SIP header tests ---
1764
1765    #[test]
1766    fn test_sip_p_asserted_identity_comma_separated() {
1767        let mut event = EslEvent::new();
1768        // RFC 3325: P-Asserted-Identity can carry two identities (one sip:, one tel:)
1769        // FreeSWITCH stores the comma-separated value as a single channel variable
1770        event.set_header(
1771            "variable_sip_P-Asserted-Identity",
1772            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1773        );
1774
1775        assert_eq!(
1776            event.variable_str("sip_P-Asserted-Identity"),
1777            Some("<sip:alice@atlanta.example.com>, <tel:+15551234567>")
1778        );
1779    }
1780
1781    #[test]
1782    fn test_sip_p_asserted_identity_array_format() {
1783        let mut event = EslEvent::new();
1784        // When FreeSWITCH stores repeated SIP headers via ARRAY format
1785        event
1786            .push_header(
1787                "variable_sip_P-Asserted-Identity",
1788                "<sip:alice@atlanta.example.com>",
1789            )
1790            .unwrap();
1791        event
1792            .push_header("variable_sip_P-Asserted-Identity", "<tel:+15551234567>")
1793            .unwrap();
1794
1795        let raw = event
1796            .header_str("variable_sip_P-Asserted-Identity")
1797            .unwrap();
1798        assert_eq!(
1799            raw,
1800            "ARRAY::<sip:alice@atlanta.example.com>|:<tel:+15551234567>"
1801        );
1802
1803        let arr = crate::variables::EslArray::parse(raw).unwrap();
1804        assert_eq!(arr.len(), 2);
1805        assert_eq!(arr.items()[0], "<sip:alice@atlanta.example.com>");
1806        assert_eq!(arr.items()[1], "<tel:+15551234567>");
1807    }
1808
1809    #[test]
1810    fn test_sip_header_with_colons_in_uri() {
1811        let mut event = EslEvent::new();
1812        // SIP URIs contain colons (sip:, sips:) which must not confuse ARRAY parsing
1813        event
1814            .push_header(
1815                "variable_sip_h_Diversion",
1816                "<sip:+15551234567@gw.example.com;reason=unconditional>",
1817            )
1818            .unwrap();
1819        event
1820            .push_header(
1821                "variable_sip_h_Diversion",
1822                "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>",
1823            )
1824            .unwrap();
1825
1826        let raw = event
1827            .header_str("variable_sip_h_Diversion")
1828            .unwrap();
1829        let arr = crate::variables::EslArray::parse(raw).unwrap();
1830        assert_eq!(arr.len(), 2);
1831        assert_eq!(
1832            arr.items()[0],
1833            "<sip:+15551234567@gw.example.com;reason=unconditional>"
1834        );
1835        assert_eq!(
1836            arr.items()[1],
1837            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>"
1838        );
1839    }
1840
1841    #[test]
1842    fn test_sip_p_asserted_identity_plain_format_round_trip() {
1843        let mut event = EslEvent::with_type(EslEventType::ChannelCreate);
1844        event.set_header("Event-Name", "CHANNEL_CREATE");
1845        event.set_header(
1846            "variable_sip_P-Asserted-Identity",
1847            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1848        );
1849
1850        let plain = event.to_plain_format();
1851        // The comma-separated value should be percent-encoded on the wire
1852        assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
1853        // Angle brackets and comma should be encoded
1854        assert!(!plain.contains("<sip:alice"));
1855    }
1856
1857    // --- Header key normalization on EslEvent ---
1858    // set_header() normalizes keys so lookups via header(EventHeader::X)
1859    // and header_str() work regardless of the casing used at insertion.
1860
1861    #[test]
1862    fn set_header_normalizes_known_enum_variant() {
1863        let mut event = EslEvent::new();
1864        event.set_header("unique-id", "abc-123");
1865        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1866    }
1867
1868    #[test]
1869    fn set_header_normalizes_codec_header() {
1870        let mut event = EslEvent::new();
1871        event.set_header("channel-read-codec-bit-rate", "128000");
1872        assert_eq!(
1873            event.header(EventHeader::ChannelReadCodecBitRate),
1874            Some("128000")
1875        );
1876    }
1877
1878    #[test]
1879    fn header_str_finds_by_original_key() {
1880        let mut event = EslEvent::new();
1881        event.set_header("unique-id", "abc-123");
1882        // Lookup by original non-canonical key should still work
1883        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1884        // Lookup by canonical key also works
1885        assert_eq!(event.header_str("Unique-ID"), Some("abc-123"));
1886    }
1887
1888    #[test]
1889    fn header_str_finds_unknown_dash_header_by_original() {
1890        let mut event = EslEvent::new();
1891        event.set_header("x-custom-header", "val");
1892        // Stored as Title-Case
1893        assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
1894        // Original key also works via alias
1895        assert_eq!(event.header_str("x-custom-header"), Some("val"));
1896    }
1897
1898    #[test]
1899    fn set_header_underscore_passthrough_preserves_sip_h() {
1900        let mut event = EslEvent::new();
1901        event.set_header("variable_sip_h_X-My-CUSTOM-Header", "val");
1902        assert_eq!(
1903            event.header_str("variable_sip_h_X-My-CUSTOM-Header"),
1904            Some("val")
1905        );
1906    }
1907
1908    #[test]
1909    fn set_header_different_casing_overwrites() {
1910        let mut event = EslEvent::new();
1911        event.set_header("Unique-ID", "first");
1912        event.set_header("unique-id", "second");
1913        // Both normalize to "Unique-ID", second overwrites first
1914        assert_eq!(event.header(EventHeader::UniqueId), Some("second"));
1915    }
1916
1917    #[test]
1918    fn remove_header_by_original_key() {
1919        let mut event = EslEvent::new();
1920        event.set_header("unique-id", "abc-123");
1921        let removed = event.remove_header("unique-id");
1922        assert_eq!(removed, Some("abc-123".to_string()));
1923        assert_eq!(event.header(EventHeader::UniqueId), None);
1924    }
1925
1926    #[test]
1927    fn remove_header_by_canonical_key() {
1928        let mut event = EslEvent::new();
1929        event.set_header("unique-id", "abc-123");
1930        let removed = event.remove_header("Unique-ID");
1931        assert_eq!(removed, Some("abc-123".to_string()));
1932        assert_eq!(event.header_str("unique-id"), None);
1933    }
1934
1935    #[test]
1936    fn serde_round_trip_preserves_canonical_lookups() {
1937        let mut event = EslEvent::new();
1938        event.set_header("unique-id", "abc-123");
1939        event.set_header("channel-read-codec-bit-rate", "128000");
1940        let json = serde_json::to_string(&event).unwrap();
1941        let deserialized: EslEvent = serde_json::from_str(&json).unwrap();
1942        assert_eq!(deserialized.header(EventHeader::UniqueId), Some("abc-123"));
1943        assert_eq!(
1944            deserialized.header(EventHeader::ChannelReadCodecBitRate),
1945            Some("128000")
1946        );
1947    }
1948
1949    #[test]
1950    fn serde_deserialize_normalizes_external_json() {
1951        let json = r#"{"event_type":null,"headers":{"unique-id":"abc-123","channel-read-codec-bit-rate":"128000"},"body":null}"#;
1952        let event: EslEvent = serde_json::from_str(json).unwrap();
1953        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1954        assert_eq!(
1955            event.header(EventHeader::ChannelReadCodecBitRate),
1956            Some("128000")
1957        );
1958        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1959    }
1960
1961    #[test]
1962    fn original_keys_rebuilt_after_serde_roundtrip() {
1963        // Real-world CODEC event quirk: switch_core_codec.c emits
1964        // `Channel-Write-Codec-Name` (Title-Case) alongside
1965        // `channel-write-codec-bit-rate` (all lowercase). The wire parser
1966        // routes every header through `set_header()` which normalizes the
1967        // key and stores non-canonical originals in the alias map so
1968        // `header_str("channel-write-codec-bit-rate")` still resolves
1969        // without an extra hash probe per lookup.
1970        //
1971        // The alias map is `#[serde(skip)]`: it's derived state. After
1972        // deserialization, the map must be rebuilt by routing every
1973        // incoming key through `set_header` — otherwise external JSON
1974        // carrying non-canonical keys (which is how FreeSWITCH's own
1975        // JSON-format events arrive over the wire) would lose non-canonical
1976        // lookup support.
1977        //
1978        // This test simulates that path: external JSON with both a
1979        // canonical and a non-canonical key present.
1980        let external_json = r#"{
1981            "event_type": null,
1982            "headers": {
1983                "Channel-Write-Codec-Name": "opus",
1984                "channel-write-codec-bit-rate": "64000",
1985                "Custom-X-Header": "preserved"
1986            },
1987            "body": null
1988        }"#;
1989        let parsed: EslEvent = serde_json::from_str(external_json).unwrap();
1990
1991        // Canonical lookup via the typed enum — always works because
1992        // set_header normalizes into the canonical form.
1993        assert_eq!(
1994            parsed.header(EventHeader::ChannelWriteCodecName),
1995            Some("opus")
1996        );
1997        assert_eq!(
1998            parsed.header(EventHeader::ChannelWriteCodecBitRate),
1999            Some("64000")
2000        );
2001
2002        // Non-canonical lookup of the bit-rate key — only works if the
2003        // alias map was rebuilt during deserialization.
2004        assert_eq!(
2005            parsed.header_str("channel-write-codec-bit-rate"),
2006            Some("64000")
2007        );
2008        // And canonical form of the same key still works.
2009        assert_eq!(
2010            parsed.header_str("Channel-Write-Codec-Bit-Rate"),
2011            Some("64000")
2012        );
2013
2014        // Headers the library has no enum variant for pass through the
2015        // title-case fallback path; both forms resolve.
2016        assert_eq!(parsed.header_str("Custom-X-Header"), Some("preserved"));
2017
2018        // And a round-trip of our own serialized output preserves the
2019        // canonical lookups (no aliases needed — we write canonical keys).
2020        let json = serde_json::to_string(&parsed).unwrap();
2021        let re_parsed: EslEvent = serde_json::from_str(&json).unwrap();
2022        assert_eq!(
2023            re_parsed.header(EventHeader::ChannelWriteCodecBitRate),
2024            Some("64000")
2025        );
2026    }
2027
2028    #[test]
2029    fn test_event_typed_accessors_invalid_values() {
2030        let mut event = EslEvent::new();
2031        event.set_header("Channel-State", "BOGUS");
2032        event.set_header("Channel-State-Number", "999");
2033        event.set_header("Channel-Call-State", "BOGUS");
2034        event.set_header("Answer-State", "bogus");
2035        event.set_header("Call-Direction", "bogus");
2036        assert!(event
2037            .channel_state()
2038            .is_err());
2039        assert!(event
2040            .channel_state_number()
2041            .is_err());
2042        assert!(event
2043            .call_state()
2044            .is_err());
2045        assert!(event
2046            .answer_state()
2047            .is_err());
2048        assert!(event
2049            .call_direction()
2050            .is_err());
2051    }
2052
2053    // --- EventSubscription tests ---
2054
2055    #[test]
2056    fn new_creates_empty() {
2057        let sub = EventSubscription::new(EventFormat::Plain);
2058        assert!(sub.is_empty());
2059        assert!(!sub.is_all());
2060        assert_eq!(sub.format(), EventFormat::Plain);
2061        assert!(sub
2062            .event_types()
2063            .is_empty());
2064        assert!(sub
2065            .custom_subclass_list()
2066            .is_empty());
2067        assert!(sub
2068            .filters()
2069            .is_empty());
2070    }
2071
2072    #[test]
2073    fn all_creates_all() {
2074        let sub = EventSubscription::all(EventFormat::Json);
2075        assert!(sub.is_all());
2076        assert!(!sub.is_empty());
2077        assert_eq!(sub.to_event_string(), Some("ALL".to_string()));
2078    }
2079
2080    #[test]
2081    fn event_string_typed_only() {
2082        let sub = EventSubscription::new(EventFormat::Plain)
2083            .event(EslEventType::ChannelCreate)
2084            .event(EslEventType::ChannelAnswer);
2085        assert_eq!(
2086            sub.to_event_string(),
2087            Some("CHANNEL_CREATE CHANNEL_ANSWER".to_string())
2088        );
2089    }
2090
2091    #[test]
2092    fn event_string_custom_only() {
2093        let sub = EventSubscription::new(EventFormat::Plain)
2094            .custom_subclass("sofia::register")
2095            .unwrap()
2096            .custom_subclass("sofia::unregister")
2097            .unwrap();
2098        assert_eq!(
2099            sub.to_event_string(),
2100            Some("CUSTOM sofia::register sofia::unregister".to_string())
2101        );
2102    }
2103
2104    #[test]
2105    fn event_string_mixed() {
2106        let sub = EventSubscription::new(EventFormat::Plain)
2107            .event(EslEventType::Heartbeat)
2108            .custom_subclass("sofia::register")
2109            .unwrap();
2110        assert_eq!(
2111            sub.to_event_string(),
2112            Some("HEARTBEAT CUSTOM sofia::register".to_string())
2113        );
2114    }
2115
2116    #[test]
2117    fn event_string_custom_not_duplicated() {
2118        let sub = EventSubscription::new(EventFormat::Plain)
2119            .event(EslEventType::Custom)
2120            .custom_subclass("sofia::register")
2121            .unwrap();
2122        // Should not have "CUSTOM" twice
2123        assert_eq!(
2124            sub.to_event_string(),
2125            Some("CUSTOM sofia::register".to_string())
2126        );
2127    }
2128
2129    #[test]
2130    fn event_string_empty_is_none() {
2131        let sub = EventSubscription::new(EventFormat::Plain);
2132        assert_eq!(sub.to_event_string(), None);
2133    }
2134
2135    #[test]
2136    fn filters_preserve_order() {
2137        let sub = EventSubscription::new(EventFormat::Plain)
2138            .filter(EventHeader::CallDirection, "inbound")
2139            .unwrap()
2140            .filter_raw("X-Custom", "value1")
2141            .unwrap()
2142            .filter(EventHeader::ChannelState, "CS_EXECUTE")
2143            .unwrap();
2144        assert_eq!(
2145            sub.filters(),
2146            &[
2147                ("Call-Direction".to_string(), "inbound".to_string()),
2148                ("X-Custom".to_string(), "value1".to_string()),
2149                ("Channel-State".to_string(), "CS_EXECUTE".to_string()),
2150            ]
2151        );
2152    }
2153
2154    #[test]
2155    fn builder_chain() {
2156        let sub = EventSubscription::new(EventFormat::Plain)
2157            .events(EslEventType::CHANNEL_EVENTS)
2158            .event(EslEventType::Heartbeat)
2159            .custom_subclass("sofia::register")
2160            .unwrap()
2161            .filter(EventHeader::CallDirection, "inbound")
2162            .unwrap()
2163            .with_format(EventFormat::Json);
2164
2165        assert_eq!(sub.format(), EventFormat::Json);
2166        assert!(!sub.is_empty());
2167        assert!(!sub.is_all());
2168        assert!(sub
2169            .event_types()
2170            .contains(&EslEventType::ChannelCreate));
2171        assert!(sub
2172            .event_types()
2173            .contains(&EslEventType::Heartbeat));
2174        assert_eq!(sub.custom_subclass_list(), &["sofia::register"]);
2175        assert_eq!(
2176            sub.filters()
2177                .len(),
2178            1
2179        );
2180    }
2181
2182    #[test]
2183    fn serde_round_trip_subscription() {
2184        let sub = EventSubscription::new(EventFormat::Plain)
2185            .event(EslEventType::ChannelCreate)
2186            .event(EslEventType::Heartbeat)
2187            .custom_subclass("sofia::register")
2188            .unwrap()
2189            .filter(EventHeader::CallDirection, "inbound")
2190            .unwrap();
2191
2192        let json = serde_json::to_string(&sub).unwrap();
2193        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
2194        assert_eq!(sub, deserialized);
2195    }
2196
2197    #[test]
2198    fn serde_rejects_invalid_subclass() {
2199        let json =
2200            r#"{"format":"Plain","events":[],"custom_subclasses":["bad subclass"],"filters":[]}"#;
2201        let result: Result<EventSubscription, _> = serde_json::from_str(json);
2202        assert!(result.is_err());
2203        let err = result
2204            .unwrap_err()
2205            .to_string();
2206        assert!(err.contains("space"), "error should mention space: {err}");
2207    }
2208
2209    #[test]
2210    fn serde_rejects_newline_in_filter() {
2211        let json = r#"{"format":"Plain","events":[],"custom_subclasses":[],"filters":[["Header","val\n"]]}"#;
2212        let result: Result<EventSubscription, _> = serde_json::from_str(json);
2213        assert!(result.is_err());
2214        let err = result
2215            .unwrap_err()
2216            .to_string();
2217        assert!(
2218            err.contains("newline"),
2219            "error should mention newline: {err}"
2220        );
2221    }
2222
2223    #[test]
2224    fn custom_subclass_rejects_space() {
2225        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad subclass");
2226        assert!(result.is_err());
2227    }
2228
2229    #[test]
2230    fn custom_subclass_rejects_newline() {
2231        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("bad\nsubclass");
2232        assert!(result.is_err());
2233    }
2234
2235    #[test]
2236    fn custom_subclass_rejects_empty() {
2237        let result = EventSubscription::new(EventFormat::Plain).custom_subclass("");
2238        assert!(result.is_err());
2239    }
2240
2241    #[test]
2242    fn filter_raw_rejects_newline_in_header() {
2243        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Bad\nHeader", "value");
2244        assert!(result.is_err());
2245    }
2246
2247    #[test]
2248    fn filter_raw_rejects_newline_in_value() {
2249        let result = EventSubscription::new(EventFormat::Plain).filter_raw("Header", "bad\nvalue");
2250        assert!(result.is_err());
2251    }
2252
2253    #[test]
2254    fn filter_typed_rejects_newline_in_value() {
2255        let result = EventSubscription::new(EventFormat::Plain)
2256            .filter(EventHeader::CallDirection, "bad\nvalue");
2257        assert!(result.is_err());
2258    }
2259
2260    #[test]
2261    fn sofia_event_single() {
2262        let sub =
2263            EventSubscription::new(EventFormat::Plain).sofia_event(SofiaEventSubclass::Register);
2264        assert_eq!(
2265            sub.to_event_string(),
2266            Some("CUSTOM sofia::register".to_string())
2267        );
2268    }
2269
2270    #[test]
2271    fn sofia_events_group() {
2272        let sub = EventSubscription::new(EventFormat::Plain)
2273            .sofia_events(SofiaEventSubclass::GATEWAY_EVENTS);
2274        let event_str = sub
2275            .to_event_string()
2276            .unwrap();
2277        assert!(event_str.starts_with("CUSTOM"));
2278        assert!(event_str.contains("sofia::gateway_state"));
2279        assert!(event_str.contains("sofia::gateway_add"));
2280        assert!(event_str.contains("sofia::gateway_delete"));
2281        assert!(event_str.contains("sofia::gateway_invalid_digest_req"));
2282    }
2283
2284    #[test]
2285    fn event_raw_wire_string() {
2286        let sub = EventSubscription::new(EventFormat::Plain)
2287            .event(EslEventType::Heartbeat)
2288            .event_raw("NEW_EVENT_NOT_IN_ENUM")
2289            .unwrap();
2290        assert_eq!(
2291            sub.to_event_string(),
2292            Some("HEARTBEAT NEW_EVENT_NOT_IN_ENUM".to_string())
2293        );
2294    }
2295
2296    #[test]
2297    fn events_raw_wire_string() {
2298        let sub = EventSubscription::new(EventFormat::Plain)
2299            .events_raw(["FUTURE_A", "FUTURE_B"])
2300            .unwrap();
2301        assert_eq!(sub.to_event_string(), Some("FUTURE_A FUTURE_B".to_string()));
2302    }
2303
2304    #[test]
2305    fn event_raw_with_custom_subclass() {
2306        let sub = EventSubscription::new(EventFormat::Plain)
2307            .event_raw("NEW_EVENT")
2308            .unwrap()
2309            .custom_subclass("sofia::register")
2310            .unwrap();
2311        assert_eq!(
2312            sub.to_event_string(),
2313            Some("NEW_EVENT CUSTOM sofia::register".to_string())
2314        );
2315    }
2316
2317    #[test]
2318    fn event_raw_rejects_newline() {
2319        assert!(EventSubscription::new(EventFormat::Plain)
2320            .event_raw("bad\nevent")
2321            .is_err());
2322    }
2323
2324    #[test]
2325    fn event_raw_rejects_space() {
2326        assert!(EventSubscription::new(EventFormat::Plain)
2327            .event_raw("bad event")
2328            .is_err());
2329    }
2330
2331    #[test]
2332    fn event_raw_rejects_empty() {
2333        assert!(EventSubscription::new(EventFormat::Plain)
2334            .event_raw("")
2335            .is_err());
2336    }
2337
2338    #[test]
2339    fn events_raw_errors_on_first_invalid() {
2340        let result =
2341            EventSubscription::new(EventFormat::Plain).events_raw(["GOOD", "bad event", "OTHER"]);
2342        assert!(result.is_err());
2343    }
2344
2345    #[test]
2346    fn event_types_raw_mut_mutable() {
2347        let mut sub = EventSubscription::new(EventFormat::Plain);
2348        sub.event_types_raw_mut()
2349            .push("DIRECT_PUSH".to_string());
2350        assert_eq!(sub.event_types_raw(), &["DIRECT_PUSH".to_string()]);
2351    }
2352
2353    #[test]
2354    fn is_empty_sees_raw_events() {
2355        let sub = EventSubscription::new(EventFormat::Plain)
2356            .event_raw("ONLY_RAW")
2357            .unwrap();
2358        assert!(!sub.is_empty());
2359    }
2360
2361    #[test]
2362    fn serde_round_trip_with_raw_events() {
2363        let sub = EventSubscription::new(EventFormat::Plain)
2364            .event(EslEventType::ChannelCreate)
2365            .event_raw("FUTURE_EVENT")
2366            .unwrap()
2367            .custom_subclass("sofia::register")
2368            .unwrap();
2369
2370        let json = serde_json::to_string(&sub).unwrap();
2371        let deserialized: EventSubscription = serde_json::from_str(&json).unwrap();
2372        assert_eq!(sub, deserialized);
2373    }
2374
2375    #[test]
2376    fn serde_rejects_invalid_raw_event() {
2377        let json = r#"{"format":"Plain","events":[],"raw_events":["bad event"],"custom_subclasses":[],"filters":[]}"#;
2378        let result: Result<EventSubscription, _> = serde_json::from_str(json);
2379        assert!(result.is_err());
2380    }
2381
2382    #[test]
2383    fn serde_missing_raw_events_field_defaults_to_empty() {
2384        // Back-compat: configs written before raw_events was added must still
2385        // deserialize.
2386        let json =
2387            r#"{"format":"Plain","events":["Heartbeat"],"custom_subclasses":[],"filters":[]}"#;
2388        let sub: EventSubscription = serde_json::from_str(json).unwrap();
2389        assert!(sub
2390            .event_types_raw()
2391            .is_empty());
2392    }
2393
2394    #[test]
2395    fn sofia_event_mixed_with_typed_events() {
2396        let sub = EventSubscription::new(EventFormat::Plain)
2397            .event(EslEventType::Heartbeat)
2398            .sofia_event(SofiaEventSubclass::GatewayState);
2399        assert_eq!(
2400            sub.to_event_string(),
2401            Some("HEARTBEAT CUSTOM sofia::gateway_state".to_string())
2402        );
2403    }
2404}