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