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