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::variables::EslArray;
6use indexmap::IndexMap;
7use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
8use std::fmt;
9use std::str::FromStr;
10
11/// Event format types supported by FreeSWITCH ESL
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[non_exhaustive]
15pub enum EventFormat {
16    /// Plain text format (default)
17    Plain,
18    /// JSON format
19    Json,
20    /// XML format
21    Xml,
22}
23
24impl EventFormat {
25    /// Determine event format from a Content-Type header value.
26    ///
27    /// Returns `Err` for unrecognized content types to avoid silently
28    /// misparsing events if FreeSWITCH adds a new format.
29    pub fn from_content_type(ct: &str) -> Result<Self, ParseEventFormatError> {
30        match ct {
31            "text/event-json" => Ok(Self::Json),
32            "text/event-xml" => Ok(Self::Xml),
33            "text/event-plain" => Ok(Self::Plain),
34            _ => Err(ParseEventFormatError(ct.to_string())),
35        }
36    }
37}
38
39impl fmt::Display for EventFormat {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            EventFormat::Plain => write!(f, "plain"),
43            EventFormat::Json => write!(f, "json"),
44            EventFormat::Xml => write!(f, "xml"),
45        }
46    }
47}
48
49impl FromStr for EventFormat {
50    type Err = ParseEventFormatError;
51
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        if s.eq_ignore_ascii_case("plain") {
54            Ok(Self::Plain)
55        } else if s.eq_ignore_ascii_case("json") {
56            Ok(Self::Json)
57        } else if s.eq_ignore_ascii_case("xml") {
58            Ok(Self::Xml)
59        } else {
60            Err(ParseEventFormatError(s.to_string()))
61        }
62    }
63}
64
65/// Error returned when parsing an invalid event format string.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ParseEventFormatError(pub String);
68
69impl fmt::Display for ParseEventFormatError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "unknown event format: {}", self.0)
72    }
73}
74
75impl std::error::Error for ParseEventFormatError {}
76
77/// Generates `EslEventType` enum with `Display`, `FromStr`, `as_str`, and `parse_event_type`.
78macro_rules! esl_event_types {
79    (
80        $(
81            $(#[$attr:meta])*
82            $variant:ident => $wire:literal
83        ),+ $(,)?
84        ;
85        // Extra variants not in the main match (after All)
86        $(
87            $(#[$extra_attr:meta])*
88            $extra_variant:ident => $extra_wire:literal
89        ),* $(,)?
90    ) => {
91        /// FreeSWITCH event types matching the canonical order from `esl_event.h`
92        /// and `switch_event.c` EVENT_NAMES[].
93        ///
94        /// Variant names are the canonical wire names (e.g. `ChannelCreate` = `CHANNEL_CREATE`).
95        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97        #[non_exhaustive]
98        #[allow(missing_docs)]
99        pub enum EslEventType {
100            $(
101                $(#[$attr])*
102                $variant,
103            )+
104            $(
105                $(#[$extra_attr])*
106                $extra_variant,
107            )*
108        }
109
110        impl fmt::Display for EslEventType {
111            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112                f.write_str(self.as_str())
113            }
114        }
115
116        impl EslEventType {
117            /// Returns the canonical wire name as a static string slice.
118            pub const fn as_str(&self) -> &'static str {
119                match self {
120                    $( EslEventType::$variant => $wire, )+
121                    $( EslEventType::$extra_variant => $extra_wire, )*
122                }
123            }
124
125            /// Parse event type from wire name (canonical case).
126            pub fn parse_event_type(s: &str) -> Option<Self> {
127                match s {
128                    $( $wire => Some(EslEventType::$variant), )+
129                    $( $extra_wire => Some(EslEventType::$extra_variant), )*
130                    _ => None,
131                }
132            }
133        }
134
135        impl FromStr for EslEventType {
136            type Err = ParseEventTypeError;
137
138            fn from_str(s: &str) -> Result<Self, Self::Err> {
139                Self::parse_event_type(s).ok_or_else(|| ParseEventTypeError(s.to_string()))
140            }
141        }
142    };
143}
144
145esl_event_types! {
146    Custom => "CUSTOM",
147    Clone => "CLONE",
148    ChannelCreate => "CHANNEL_CREATE",
149    ChannelDestroy => "CHANNEL_DESTROY",
150    ChannelState => "CHANNEL_STATE",
151    ChannelCallstate => "CHANNEL_CALLSTATE",
152    ChannelAnswer => "CHANNEL_ANSWER",
153    ChannelHangup => "CHANNEL_HANGUP",
154    ChannelHangupComplete => "CHANNEL_HANGUP_COMPLETE",
155    ChannelExecute => "CHANNEL_EXECUTE",
156    ChannelExecuteComplete => "CHANNEL_EXECUTE_COMPLETE",
157    ChannelHold => "CHANNEL_HOLD",
158    ChannelUnhold => "CHANNEL_UNHOLD",
159    ChannelBridge => "CHANNEL_BRIDGE",
160    ChannelUnbridge => "CHANNEL_UNBRIDGE",
161    ChannelProgress => "CHANNEL_PROGRESS",
162    ChannelProgressMedia => "CHANNEL_PROGRESS_MEDIA",
163    ChannelOutgoing => "CHANNEL_OUTGOING",
164    ChannelPark => "CHANNEL_PARK",
165    ChannelUnpark => "CHANNEL_UNPARK",
166    ChannelApplication => "CHANNEL_APPLICATION",
167    ChannelOriginate => "CHANNEL_ORIGINATE",
168    ChannelUuid => "CHANNEL_UUID",
169    Api => "API",
170    Log => "LOG",
171    InboundChan => "INBOUND_CHAN",
172    OutboundChan => "OUTBOUND_CHAN",
173    Startup => "STARTUP",
174    Shutdown => "SHUTDOWN",
175    Publish => "PUBLISH",
176    Unpublish => "UNPUBLISH",
177    Talk => "TALK",
178    Notalk => "NOTALK",
179    SessionCrash => "SESSION_CRASH",
180    ModuleLoad => "MODULE_LOAD",
181    ModuleUnload => "MODULE_UNLOAD",
182    Dtmf => "DTMF",
183    Message => "MESSAGE",
184    PresenceIn => "PRESENCE_IN",
185    NotifyIn => "NOTIFY_IN",
186    PresenceOut => "PRESENCE_OUT",
187    PresenceProbe => "PRESENCE_PROBE",
188    MessageWaiting => "MESSAGE_WAITING",
189    MessageQuery => "MESSAGE_QUERY",
190    Roster => "ROSTER",
191    Codec => "CODEC",
192    BackgroundJob => "BACKGROUND_JOB",
193    DetectedSpeech => "DETECTED_SPEECH",
194    DetectedTone => "DETECTED_TONE",
195    PrivateCommand => "PRIVATE_COMMAND",
196    Heartbeat => "HEARTBEAT",
197    Trap => "TRAP",
198    AddSchedule => "ADD_SCHEDULE",
199    DelSchedule => "DEL_SCHEDULE",
200    ExeSchedule => "EXE_SCHEDULE",
201    ReSchedule => "RE_SCHEDULE",
202    ReloadXml => "RELOADXML",
203    Notify => "NOTIFY",
204    PhoneFeature => "PHONE_FEATURE",
205    PhoneFeatureSubscribe => "PHONE_FEATURE_SUBSCRIBE",
206    SendMessage => "SEND_MESSAGE",
207    RecvMessage => "RECV_MESSAGE",
208    RequestParams => "REQUEST_PARAMS",
209    ChannelData => "CHANNEL_DATA",
210    General => "GENERAL",
211    Command => "COMMAND",
212    SessionHeartbeat => "SESSION_HEARTBEAT",
213    ClientDisconnected => "CLIENT_DISCONNECTED",
214    ServerDisconnected => "SERVER_DISCONNECTED",
215    SendInfo => "SEND_INFO",
216    RecvInfo => "RECV_INFO",
217    RecvRtcpMessage => "RECV_RTCP_MESSAGE",
218    SendRtcpMessage => "SEND_RTCP_MESSAGE",
219    CallSecure => "CALL_SECURE",
220    Nat => "NAT",
221    RecordStart => "RECORD_START",
222    RecordStop => "RECORD_STOP",
223    PlaybackStart => "PLAYBACK_START",
224    PlaybackStop => "PLAYBACK_STOP",
225    CallUpdate => "CALL_UPDATE",
226    Failure => "FAILURE",
227    SocketData => "SOCKET_DATA",
228    MediaBugStart => "MEDIA_BUG_START",
229    MediaBugStop => "MEDIA_BUG_STOP",
230    ConferenceDataQuery => "CONFERENCE_DATA_QUERY",
231    ConferenceData => "CONFERENCE_DATA",
232    CallSetupReq => "CALL_SETUP_REQ",
233    CallSetupResult => "CALL_SETUP_RESULT",
234    CallDetail => "CALL_DETAIL",
235    DeviceState => "DEVICE_STATE",
236    Text => "TEXT",
237    ShutdownRequested => "SHUTDOWN_REQUESTED",
238    /// Subscribe to all events
239    All => "ALL";
240    // --- Not in libs/esl/ EVENT_NAMES[], only in switch_event.c ---
241    // check-event-types.sh stops scanning at the All variant above.
242    /// Present in `switch_event.c` but not in `libs/esl/` EVENT_NAMES[].
243    StartRecording => "START_RECORDING",
244}
245
246// -- Event group constants --------------------------------------------------
247//
248// Predefined slices for common subscription patterns. Pass directly to
249// `EslClient::subscribe_events()`.
250//
251// MAINTENANCE: when adding new `EslEventType` variants, check whether they
252// belong in any of these groups and update accordingly.
253
254impl EslEventType {
255    /// Every `CHANNEL_*` event type.
256    ///
257    /// Covers the full channel lifecycle: creation, state changes, execution,
258    /// bridging, hold, park, progress, originate, and destruction.
259    ///
260    /// ```rust
261    /// use freeswitch_types::EslEventType;
262    /// assert!(EslEventType::CHANNEL_EVENTS.contains(&EslEventType::ChannelCreate));
263    /// assert!(EslEventType::CHANNEL_EVENTS.contains(&EslEventType::ChannelHangupComplete));
264    /// ```
265    pub const CHANNEL_EVENTS: &[EslEventType] = &[
266        EslEventType::ChannelCreate,
267        EslEventType::ChannelDestroy,
268        EslEventType::ChannelState,
269        EslEventType::ChannelCallstate,
270        EslEventType::ChannelAnswer,
271        EslEventType::ChannelHangup,
272        EslEventType::ChannelHangupComplete,
273        EslEventType::ChannelExecute,
274        EslEventType::ChannelExecuteComplete,
275        EslEventType::ChannelHold,
276        EslEventType::ChannelUnhold,
277        EslEventType::ChannelBridge,
278        EslEventType::ChannelUnbridge,
279        EslEventType::ChannelProgress,
280        EslEventType::ChannelProgressMedia,
281        EslEventType::ChannelOutgoing,
282        EslEventType::ChannelPark,
283        EslEventType::ChannelUnpark,
284        EslEventType::ChannelApplication,
285        EslEventType::ChannelOriginate,
286        EslEventType::ChannelUuid,
287        EslEventType::ChannelData,
288    ];
289
290    /// In-call events: DTMF, VAD speech detection, media security, and call updates.
291    ///
292    /// Events that fire during an established call, tied to RTP/media activity
293    /// rather than signaling state transitions.
294    ///
295    /// ```rust
296    /// use freeswitch_types::EslEventType;
297    /// assert!(EslEventType::IN_CALL_EVENTS.contains(&EslEventType::Dtmf));
298    /// assert!(EslEventType::IN_CALL_EVENTS.contains(&EslEventType::Talk));
299    /// ```
300    pub const IN_CALL_EVENTS: &[EslEventType] = &[
301        EslEventType::Dtmf,
302        EslEventType::Talk,
303        EslEventType::Notalk,
304        EslEventType::CallSecure,
305        EslEventType::CallUpdate,
306        EslEventType::RecvRtcpMessage,
307        EslEventType::SendRtcpMessage,
308    ];
309
310    /// Media-related events: playback, recording, media bugs, and detection.
311    ///
312    /// Useful for IVR applications that need to track media operations without
313    /// subscribing to the full channel lifecycle.
314    ///
315    /// ```rust
316    /// use freeswitch_types::EslEventType;
317    /// assert!(EslEventType::MEDIA_EVENTS.contains(&EslEventType::PlaybackStart));
318    /// assert!(EslEventType::MEDIA_EVENTS.contains(&EslEventType::DetectedSpeech));
319    /// ```
320    pub const MEDIA_EVENTS: &[EslEventType] = &[
321        EslEventType::PlaybackStart,
322        EslEventType::PlaybackStop,
323        EslEventType::RecordStart,
324        EslEventType::RecordStop,
325        EslEventType::StartRecording,
326        EslEventType::MediaBugStart,
327        EslEventType::MediaBugStop,
328        EslEventType::DetectedSpeech,
329        EslEventType::DetectedTone,
330    ];
331
332    /// Presence and messaging events.
333    ///
334    /// For applications that track user presence (BLF, buddy lists) or
335    /// message-waiting indicators (voicemail MWI).
336    ///
337    /// ```rust
338    /// use freeswitch_types::EslEventType;
339    /// assert!(EslEventType::PRESENCE_EVENTS.contains(&EslEventType::PresenceIn));
340    /// assert!(EslEventType::PRESENCE_EVENTS.contains(&EslEventType::MessageWaiting));
341    /// ```
342    pub const PRESENCE_EVENTS: &[EslEventType] = &[
343        EslEventType::PresenceIn,
344        EslEventType::PresenceOut,
345        EslEventType::PresenceProbe,
346        EslEventType::MessageWaiting,
347        EslEventType::MessageQuery,
348        EslEventType::Roster,
349    ];
350
351    /// System lifecycle events.
352    ///
353    /// Server startup/shutdown, heartbeats, module loading, and XML reloads.
354    /// Useful for monitoring dashboards and operational tooling.
355    ///
356    /// ```rust
357    /// use freeswitch_types::EslEventType;
358    /// assert!(EslEventType::SYSTEM_EVENTS.contains(&EslEventType::Heartbeat));
359    /// assert!(EslEventType::SYSTEM_EVENTS.contains(&EslEventType::Shutdown));
360    /// ```
361    pub const SYSTEM_EVENTS: &[EslEventType] = &[
362        EslEventType::Startup,
363        EslEventType::Shutdown,
364        EslEventType::ShutdownRequested,
365        EslEventType::Heartbeat,
366        EslEventType::SessionHeartbeat,
367        EslEventType::SessionCrash,
368        EslEventType::ModuleLoad,
369        EslEventType::ModuleUnload,
370        EslEventType::ReloadXml,
371    ];
372
373    /// Conference-related events.
374    ///
375    /// ```rust
376    /// use freeswitch_types::EslEventType;
377    /// assert!(EslEventType::CONFERENCE_EVENTS.contains(&EslEventType::ConferenceData));
378    /// ```
379    pub const CONFERENCE_EVENTS: &[EslEventType] = &[
380        EslEventType::ConferenceDataQuery,
381        EslEventType::ConferenceData,
382    ];
383}
384
385/// Error returned when parsing an unknown event type string.
386#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct ParseEventTypeError(pub String);
388
389impl fmt::Display for ParseEventTypeError {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        write!(f, "unknown event type: {}", self.0)
392    }
393}
394
395impl std::error::Error for ParseEventTypeError {}
396
397/// Event priority levels matching FreeSWITCH `esl_priority_t`
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
399#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
400#[non_exhaustive]
401pub enum EslEventPriority {
402    /// Default priority.
403    Normal,
404    /// Lower than normal.
405    Low,
406    /// Higher than normal.
407    High,
408}
409
410impl fmt::Display for EslEventPriority {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self {
413            EslEventPriority::Normal => write!(f, "NORMAL"),
414            EslEventPriority::Low => write!(f, "LOW"),
415            EslEventPriority::High => write!(f, "HIGH"),
416        }
417    }
418}
419
420/// Error returned when parsing an invalid priority string.
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct ParsePriorityError(pub String);
423
424impl fmt::Display for ParsePriorityError {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        write!(f, "unknown priority: {}", self.0)
427    }
428}
429
430impl std::error::Error for ParsePriorityError {}
431
432impl FromStr for EslEventPriority {
433    type Err = ParsePriorityError;
434
435    fn from_str(s: &str) -> Result<Self, Self::Err> {
436        match s {
437            "NORMAL" => Ok(EslEventPriority::Normal),
438            "LOW" => Ok(EslEventPriority::Low),
439            "HIGH" => Ok(EslEventPriority::High),
440            _ => Err(ParsePriorityError(s.to_string())),
441        }
442    }
443}
444
445/// ESL Event structure containing headers and optional body
446#[derive(Debug, Clone, Eq)]
447#[cfg_attr(feature = "serde", derive(serde::Serialize))]
448pub struct EslEvent {
449    event_type: Option<EslEventType>,
450    headers: IndexMap<String, String>,
451    #[cfg_attr(feature = "serde", serde(skip))]
452    original_keys: IndexMap<String, String>,
453    body: Option<String>,
454}
455
456impl EslEvent {
457    /// Create a new empty event
458    pub fn new() -> Self {
459        Self {
460            event_type: None,
461            headers: IndexMap::new(),
462            original_keys: IndexMap::new(),
463            body: None,
464        }
465    }
466
467    /// Create event with specified type
468    pub fn with_type(event_type: EslEventType) -> Self {
469        Self {
470            event_type: Some(event_type),
471            headers: IndexMap::new(),
472            original_keys: IndexMap::new(),
473            body: None,
474        }
475    }
476
477    /// Parsed event type, if recognized.
478    pub fn event_type(&self) -> Option<EslEventType> {
479        self.event_type
480    }
481
482    /// Override the event type.
483    pub fn set_event_type(&mut self, event_type: Option<EslEventType>) {
484        self.event_type = event_type;
485    }
486
487    /// Look up a header by its [`EventHeader`] enum variant (case-sensitive).
488    ///
489    /// For headers not covered by `EventHeader`, use [`header_str()`](Self::header_str).
490    pub fn header(&self, name: EventHeader) -> Option<&str> {
491        self.headers
492            .get(name.as_str())
493            .map(|s| s.as_str())
494    }
495
496    /// Look up a header by name, trying the canonical key first then falling
497    /// back through the alias map for non-canonical lookups.
498    ///
499    /// Use [`header()`](Self::header) with an [`EventHeader`] variant for known
500    /// headers. This method is for headers not (yet) covered by the enum,
501    /// such as custom `X-` headers or FreeSWITCH headers added after this
502    /// library was published.
503    pub fn header_str(&self, name: &str) -> Option<&str> {
504        self.headers
505            .get(name)
506            .or_else(|| {
507                self.original_keys
508                    .get(name)
509                    .and_then(|normalized| {
510                        self.headers
511                            .get(normalized)
512                    })
513            })
514            .map(|s| s.as_str())
515    }
516
517    /// Look up a channel variable by its bare name.
518    ///
519    /// Equivalent to [`variable()`](Self::variable) but matches the
520    /// [`HeaderLookup`] trait signature.
521    pub fn variable_str(&self, name: &str) -> Option<&str> {
522        let key = format!("variable_{}", name);
523        self.header_str(&key)
524    }
525
526    /// All headers as a map.
527    pub fn headers(&self) -> &IndexMap<String, String> {
528        &self.headers
529    }
530
531    /// Set or overwrite a header, normalizing the key.
532    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
533        let original = name.into();
534        let normalized = normalize_header_key(&original);
535        if original != normalized {
536            self.original_keys
537                .insert(original, normalized.clone());
538        }
539        self.headers
540            .insert(normalized, value.into());
541    }
542
543    /// Remove a header, returning its value if it existed.
544    ///
545    /// Accepts both canonical and original (non-normalized) key names.
546    pub fn remove_header(&mut self, name: impl AsRef<str>) -> Option<String> {
547        let name = name.as_ref();
548        if let Some(value) = self
549            .headers
550            .shift_remove(name)
551        {
552            return Some(value);
553        }
554        if let Some(normalized) = self
555            .original_keys
556            .shift_remove(name)
557        {
558            return self
559                .headers
560                .shift_remove(&normalized);
561        }
562        None
563    }
564
565    /// Event body (the content after the blank line in plain-text events).
566    pub fn body(&self) -> Option<&str> {
567        self.body
568            .as_deref()
569    }
570
571    /// Set the event body.
572    pub fn set_body(&mut self, body: impl Into<String>) {
573        self.body = Some(body.into());
574    }
575
576    /// Sets the `priority` header carried on the event.
577    ///
578    /// FreeSWITCH stores this as metadata but does **not** use it for dispatch
579    /// ordering — all events are delivered FIFO regardless of priority.
580    pub fn set_priority(&mut self, priority: EslEventPriority) {
581        self.set_header(EventHeader::Priority.as_str(), priority.to_string());
582    }
583
584    /// Append a value to a multi-value header (PUSH semantics).
585    ///
586    /// If the header doesn't exist, sets it as a plain value.
587    /// If it exists as a plain value, converts to `ARRAY::old|:new`.
588    /// If it already has an `ARRAY::` prefix, appends the new value.
589    ///
590    /// ```
591    /// # use freeswitch_types::EslEvent;
592    /// let mut event = EslEvent::new();
593    /// event.push_header("X-Test", "first");
594    /// event.push_header("X-Test", "second");
595    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
596    /// ```
597    pub fn push_header(&mut self, name: &str, value: &str) {
598        self.stack_header(name, value, EslArray::push);
599    }
600
601    /// Prepend a value to a multi-value header (UNSHIFT semantics).
602    ///
603    /// Same conversion rules as `push_header()`, but inserts at the front.
604    ///
605    /// ```
606    /// # use freeswitch_types::EslEvent;
607    /// let mut event = EslEvent::new();
608    /// event.set_header("X-Test", "ARRAY::b|:c");
609    /// event.unshift_header("X-Test", "a");
610    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
611    /// ```
612    pub fn unshift_header(&mut self, name: &str, value: &str) {
613        self.stack_header(name, value, EslArray::unshift);
614    }
615
616    fn stack_header(&mut self, name: &str, value: &str, op: fn(&mut EslArray, String)) {
617        match self
618            .headers
619            .get(name)
620        {
621            None => {
622                self.set_header(name, value);
623            }
624            Some(existing) => {
625                let mut arr = match EslArray::parse(existing) {
626                    Some(arr) => arr,
627                    None => EslArray::new(vec![existing.clone()]),
628                };
629                op(&mut arr, value.into());
630                self.set_header(name, arr.to_string());
631            }
632        }
633    }
634
635    /// Check whether this event matches the given type.
636    pub fn is_event_type(&self, event_type: EslEventType) -> bool {
637        self.event_type == Some(event_type)
638    }
639
640    /// Serialize to ESL plain text wire format with percent-encoded header values.
641    ///
642    /// This is the inverse of `EslParser::parse_plain_event()`. The output can
643    /// be fed back through the parser to reconstruct an equivalent `EslEvent`
644    /// (round-trip).
645    ///
646    /// Headers are emitted in insertion order (which matches wire order when the
647    /// event was parsed from the network). `Content-Length` from stored headers
648    /// is skipped and recomputed from the body if present.
649    pub fn to_plain_format(&self) -> String {
650        use std::fmt::Write;
651        let mut result = String::new();
652
653        for (key, value) in &self.headers {
654            if key == "Content-Length" {
655                continue;
656            }
657            let _ = writeln!(
658                result,
659                "{}: {}",
660                key,
661                percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
662            );
663        }
664
665        if let Some(body) = &self.body {
666            let _ = writeln!(result, "Content-Length: {}", body.len());
667            result.push('\n');
668            result.push_str(body);
669        } else {
670            result.push('\n');
671        }
672
673        result
674    }
675}
676
677impl Default for EslEvent {
678    fn default() -> Self {
679        Self::new()
680    }
681}
682
683impl HeaderLookup for EslEvent {
684    fn header_str(&self, name: &str) -> Option<&str> {
685        EslEvent::header_str(self, name)
686    }
687
688    fn variable_str(&self, name: &str) -> Option<&str> {
689        let key = format!("variable_{}", name);
690        self.header_str(&key)
691    }
692}
693
694impl PartialEq for EslEvent {
695    fn eq(&self, other: &Self) -> bool {
696        self.event_type == other.event_type
697            && self.headers == other.headers
698            && self.body == other.body
699    }
700}
701
702impl std::hash::Hash for EslEvent {
703    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
704        self.event_type
705            .hash(state);
706        for (k, v) in &self.headers {
707            k.hash(state);
708            v.hash(state);
709        }
710        self.body
711            .hash(state);
712    }
713}
714
715#[cfg(feature = "serde")]
716impl<'de> serde::Deserialize<'de> for EslEvent {
717    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
718    where
719        D: serde::Deserializer<'de>,
720    {
721        #[derive(serde::Deserialize)]
722        struct Raw {
723            event_type: Option<EslEventType>,
724            headers: IndexMap<String, String>,
725            body: Option<String>,
726        }
727        let raw = Raw::deserialize(deserializer)?;
728        let mut event = EslEvent::new();
729        event.event_type = raw.event_type;
730        event.body = raw.body;
731        for (k, v) in raw.headers {
732            event.set_header(k, v);
733        }
734        Ok(event)
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    #[test]
743    fn headers_preserve_insertion_order() {
744        let mut event = EslEvent::new();
745        event.set_header("Zebra", "last");
746        event.set_header("Alpha", "first");
747        event.set_header("Middle", "mid");
748        let keys: Vec<&str> = event
749            .headers()
750            .keys()
751            .map(|s| s.as_str())
752            .collect();
753        assert_eq!(keys, vec!["Zebra", "Alpha", "Middle"]);
754    }
755
756    #[test]
757    fn test_notify_in_parse() {
758        assert_eq!(
759            EslEventType::parse_event_type("NOTIFY_IN"),
760            Some(EslEventType::NotifyIn)
761        );
762        assert_eq!(EslEventType::parse_event_type("notify_in"), None);
763    }
764
765    #[test]
766    fn test_notify_in_display() {
767        assert_eq!(EslEventType::NotifyIn.to_string(), "NOTIFY_IN");
768    }
769
770    #[test]
771    fn test_notify_in_distinct_from_notify() {
772        assert_ne!(EslEventType::Notify, EslEventType::NotifyIn);
773        assert_ne!(
774            EslEventType::Notify.to_string(),
775            EslEventType::NotifyIn.to_string()
776        );
777    }
778
779    #[test]
780    fn test_wire_names_match_c_esl() {
781        assert_eq!(
782            EslEventType::ChannelOutgoing.to_string(),
783            "CHANNEL_OUTGOING"
784        );
785        assert_eq!(EslEventType::Api.to_string(), "API");
786        assert_eq!(EslEventType::ReloadXml.to_string(), "RELOADXML");
787        assert_eq!(EslEventType::PresenceIn.to_string(), "PRESENCE_IN");
788        assert_eq!(EslEventType::Roster.to_string(), "ROSTER");
789        assert_eq!(EslEventType::Text.to_string(), "TEXT");
790        assert_eq!(EslEventType::ReSchedule.to_string(), "RE_SCHEDULE");
791
792        assert_eq!(
793            EslEventType::parse_event_type("CHANNEL_OUTGOING"),
794            Some(EslEventType::ChannelOutgoing)
795        );
796        assert_eq!(
797            EslEventType::parse_event_type("API"),
798            Some(EslEventType::Api)
799        );
800        assert_eq!(
801            EslEventType::parse_event_type("RELOADXML"),
802            Some(EslEventType::ReloadXml)
803        );
804        assert_eq!(
805            EslEventType::parse_event_type("PRESENCE_IN"),
806            Some(EslEventType::PresenceIn)
807        );
808    }
809
810    #[test]
811    fn test_event_type_from_str() {
812        assert_eq!(
813            "CHANNEL_ANSWER".parse::<EslEventType>(),
814            Ok(EslEventType::ChannelAnswer)
815        );
816        assert!("channel_answer"
817            .parse::<EslEventType>()
818            .is_err());
819        assert!("UNKNOWN_EVENT"
820            .parse::<EslEventType>()
821            .is_err());
822    }
823
824    #[test]
825    fn test_remove_header() {
826        let mut event = EslEvent::new();
827        event.set_header("Foo", "bar");
828        event.set_header("Baz", "qux");
829
830        let removed = event.remove_header("Foo");
831        assert_eq!(removed, Some("bar".to_string()));
832        assert!(event
833            .header_str("Foo")
834            .is_none());
835        assert_eq!(event.header_str("Baz"), Some("qux"));
836
837        let removed_again = event.remove_header("Foo");
838        assert_eq!(removed_again, None);
839    }
840
841    #[test]
842    fn test_to_plain_format_basic() {
843        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
844        event.set_header("Event-Name", "HEARTBEAT");
845        event.set_header("Core-UUID", "abc-123");
846
847        let plain = event.to_plain_format();
848
849        assert!(plain.starts_with("Event-Name: "));
850        assert!(plain.contains("Core-UUID: "));
851        assert!(plain.ends_with("\n\n"));
852    }
853
854    #[test]
855    fn test_to_plain_format_percent_encoding() {
856        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
857        event.set_header("Event-Name", "HEARTBEAT");
858        event.set_header("Up-Time", "0 years, 0 days");
859
860        let plain = event.to_plain_format();
861
862        assert!(!plain.contains("0 years, 0 days"));
863        assert!(plain.contains("Up-Time: "));
864        assert!(plain.contains("%20"));
865    }
866
867    #[test]
868    fn test_to_plain_format_with_body() {
869        let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
870        event.set_header("Event-Name", "BACKGROUND_JOB");
871        event.set_header("Job-UUID", "def-456");
872        event.set_body("+OK result\n".to_string());
873
874        let plain = event.to_plain_format();
875
876        assert!(plain.contains("Content-Length: 11\n"));
877        assert!(plain.ends_with("\n\n+OK result\n"));
878    }
879
880    #[test]
881    fn test_to_plain_format_preserves_insertion_order() {
882        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
883        event.set_header("Event-Name", "HEARTBEAT");
884        event.set_header("Core-UUID", "abc-123");
885        event.set_header("FreeSWITCH-Hostname", "fs01");
886        event.set_header("Up-Time", "0 years, 1 day");
887
888        let plain = event.to_plain_format();
889        let lines: Vec<&str> = plain
890            .lines()
891            .collect();
892        assert!(lines[0].starts_with("Event-Name: "));
893        assert!(lines[1].starts_with("Core-UUID: "));
894        assert!(lines[2].starts_with("FreeSWITCH-Hostname: "));
895        assert!(lines[3].starts_with("Up-Time: "));
896    }
897
898    #[test]
899    fn test_to_plain_format_round_trip() {
900        let mut original = EslEvent::with_type(EslEventType::ChannelCreate);
901        original.set_header("Event-Name", "CHANNEL_CREATE");
902        original.set_header("Core-UUID", "abc-123");
903        original.set_header("Channel-Name", "sofia/internal/1000@example.com");
904        original.set_header("Caller-Caller-ID-Name", "Jérôme Poulin");
905        original.set_body("some body content");
906
907        let plain = original.to_plain_format();
908
909        // Simulate what EslParser::parse_plain_event does
910        let (header_section, inner_body) = if let Some(pos) = plain.find("\n\n") {
911            (&plain[..pos], Some(&plain[pos + 2..]))
912        } else {
913            (plain.as_str(), None)
914        };
915
916        let mut parsed = EslEvent::new();
917        for line in header_section.lines() {
918            let line = line.trim();
919            if line.is_empty() {
920                continue;
921            }
922            if let Some(colon_pos) = line.find(':') {
923                let key = line[..colon_pos].trim();
924                if key == "Content-Length" {
925                    continue;
926                }
927                let raw_value = line[colon_pos + 1..].trim();
928                let value = percent_encoding::percent_decode_str(raw_value)
929                    .decode_utf8()
930                    .unwrap()
931                    .into_owned();
932                parsed.set_header(key, value);
933            }
934        }
935        if let Some(ib) = inner_body {
936            if !ib.is_empty() {
937                parsed.set_body(ib);
938            }
939        }
940
941        assert_eq!(original.headers(), parsed.headers());
942        assert_eq!(original.body(), parsed.body());
943    }
944
945    #[test]
946    fn test_set_priority_normal() {
947        let mut event = EslEvent::new();
948        event.set_priority(EslEventPriority::Normal);
949        assert_eq!(
950            event
951                .priority()
952                .unwrap(),
953            Some(EslEventPriority::Normal)
954        );
955        assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
956    }
957
958    #[test]
959    fn test_set_priority_high() {
960        let mut event = EslEvent::new();
961        event.set_priority(EslEventPriority::High);
962        assert_eq!(
963            event
964                .priority()
965                .unwrap(),
966            Some(EslEventPriority::High)
967        );
968        assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
969    }
970
971    #[test]
972    fn test_priority_display() {
973        assert_eq!(EslEventPriority::Normal.to_string(), "NORMAL");
974        assert_eq!(EslEventPriority::Low.to_string(), "LOW");
975        assert_eq!(EslEventPriority::High.to_string(), "HIGH");
976    }
977
978    #[test]
979    fn test_priority_from_str() {
980        assert_eq!(
981            "NORMAL".parse::<EslEventPriority>(),
982            Ok(EslEventPriority::Normal)
983        );
984        assert_eq!("LOW".parse::<EslEventPriority>(), Ok(EslEventPriority::Low));
985        assert_eq!(
986            "HIGH".parse::<EslEventPriority>(),
987            Ok(EslEventPriority::High)
988        );
989        assert!("INVALID"
990            .parse::<EslEventPriority>()
991            .is_err());
992    }
993
994    #[test]
995    fn test_priority_from_str_rejects_wrong_case() {
996        assert!("normal"
997            .parse::<EslEventPriority>()
998            .is_err());
999        assert!("Low"
1000            .parse::<EslEventPriority>()
1001            .is_err());
1002        assert!("hIgH"
1003            .parse::<EslEventPriority>()
1004            .is_err());
1005    }
1006
1007    #[test]
1008    fn test_push_header_new() {
1009        let mut event = EslEvent::new();
1010        event.push_header("X-Test", "first");
1011        assert_eq!(event.header_str("X-Test"), Some("first"));
1012    }
1013
1014    #[test]
1015    fn test_push_header_existing_plain() {
1016        let mut event = EslEvent::new();
1017        event.set_header("X-Test", "first");
1018        event.push_header("X-Test", "second");
1019        assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
1020    }
1021
1022    #[test]
1023    fn test_push_header_existing_array() {
1024        let mut event = EslEvent::new();
1025        event.set_header("X-Test", "ARRAY::a|:b");
1026        event.push_header("X-Test", "c");
1027        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1028    }
1029
1030    #[test]
1031    fn test_unshift_header_new() {
1032        let mut event = EslEvent::new();
1033        event.unshift_header("X-Test", "only");
1034        assert_eq!(event.header_str("X-Test"), Some("only"));
1035    }
1036
1037    #[test]
1038    fn test_unshift_header_existing_array() {
1039        let mut event = EslEvent::new();
1040        event.set_header("X-Test", "ARRAY::b|:c");
1041        event.unshift_header("X-Test", "a");
1042        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
1043    }
1044
1045    #[test]
1046    fn test_sendevent_with_priority_wire_format() {
1047        let mut event = EslEvent::with_type(EslEventType::Custom);
1048        event.set_header("Event-Name", "CUSTOM");
1049        event.set_header("Event-Subclass", "test::priority");
1050        event.set_priority(EslEventPriority::High);
1051
1052        let plain = event.to_plain_format();
1053        assert!(plain.contains("priority: HIGH\n"));
1054    }
1055
1056    #[test]
1057    fn test_convenience_accessors() {
1058        let mut event = EslEvent::new();
1059        event.set_header("Channel-Name", "sofia/internal/1000@example.com");
1060        event.set_header("Caller-Caller-ID-Number", "1000");
1061        event.set_header("Caller-Caller-ID-Name", "Alice");
1062        event.set_header("Hangup-Cause", "NORMAL_CLEARING");
1063        event.set_header("Event-Subclass", "sofia::register");
1064        event.set_header("variable_sip_from_display", "Bob");
1065
1066        assert_eq!(
1067            event.channel_name(),
1068            Some("sofia/internal/1000@example.com")
1069        );
1070        assert_eq!(event.caller_id_number(), Some("1000"));
1071        assert_eq!(event.caller_id_name(), Some("Alice"));
1072        assert_eq!(
1073            event
1074                .hangup_cause()
1075                .unwrap(),
1076            Some(crate::channel::HangupCause::NormalClearing)
1077        );
1078        assert_eq!(event.event_subclass(), Some("sofia::register"));
1079        assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
1080        assert_eq!(event.variable_str("nonexistent"), None);
1081    }
1082
1083    #[test]
1084    fn test_event_format_from_str() {
1085        assert_eq!("plain".parse::<EventFormat>(), Ok(EventFormat::Plain));
1086        assert_eq!("json".parse::<EventFormat>(), Ok(EventFormat::Json));
1087        assert_eq!("xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1088        assert!("foo"
1089            .parse::<EventFormat>()
1090            .is_err());
1091    }
1092
1093    #[test]
1094    fn test_event_format_from_str_case_insensitive() {
1095        assert_eq!("PLAIN".parse::<EventFormat>(), Ok(EventFormat::Plain));
1096        assert_eq!("Json".parse::<EventFormat>(), Ok(EventFormat::Json));
1097        assert_eq!("XML".parse::<EventFormat>(), Ok(EventFormat::Xml));
1098        assert_eq!("Xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
1099    }
1100
1101    #[test]
1102    fn test_event_format_from_content_type() {
1103        assert_eq!(
1104            EventFormat::from_content_type("text/event-json"),
1105            Ok(EventFormat::Json)
1106        );
1107        assert_eq!(
1108            EventFormat::from_content_type("text/event-xml"),
1109            Ok(EventFormat::Xml)
1110        );
1111        assert_eq!(
1112            EventFormat::from_content_type("text/event-plain"),
1113            Ok(EventFormat::Plain)
1114        );
1115        assert!(EventFormat::from_content_type("unknown").is_err());
1116    }
1117
1118    // --- EslEvent accessor tests (via HeaderLookup trait) ---
1119
1120    #[test]
1121    fn test_event_channel_state_accessor() {
1122        use crate::channel::ChannelState;
1123        let mut event = EslEvent::new();
1124        event.set_header("Channel-State", "CS_EXECUTE");
1125        assert_eq!(
1126            event
1127                .channel_state()
1128                .unwrap(),
1129            Some(ChannelState::CsExecute)
1130        );
1131    }
1132
1133    #[test]
1134    fn test_event_channel_state_number_accessor() {
1135        use crate::channel::ChannelState;
1136        let mut event = EslEvent::new();
1137        event.set_header("Channel-State-Number", "4");
1138        assert_eq!(
1139            event
1140                .channel_state_number()
1141                .unwrap(),
1142            Some(ChannelState::CsExecute)
1143        );
1144    }
1145
1146    #[test]
1147    fn test_event_call_state_accessor() {
1148        use crate::channel::CallState;
1149        let mut event = EslEvent::new();
1150        event.set_header("Channel-Call-State", "ACTIVE");
1151        assert_eq!(
1152            event
1153                .call_state()
1154                .unwrap(),
1155            Some(CallState::Active)
1156        );
1157    }
1158
1159    #[test]
1160    fn test_event_answer_state_accessor() {
1161        use crate::channel::AnswerState;
1162        let mut event = EslEvent::new();
1163        event.set_header("Answer-State", "answered");
1164        assert_eq!(
1165            event
1166                .answer_state()
1167                .unwrap(),
1168            Some(AnswerState::Answered)
1169        );
1170    }
1171
1172    #[test]
1173    fn test_event_call_direction_accessor() {
1174        use crate::channel::CallDirection;
1175        let mut event = EslEvent::new();
1176        event.set_header("Call-Direction", "inbound");
1177        assert_eq!(
1178            event
1179                .call_direction()
1180                .unwrap(),
1181            Some(CallDirection::Inbound)
1182        );
1183    }
1184
1185    #[test]
1186    fn test_event_typed_accessors_missing_headers() {
1187        let event = EslEvent::new();
1188        assert_eq!(
1189            event
1190                .channel_state()
1191                .unwrap(),
1192            None
1193        );
1194        assert_eq!(
1195            event
1196                .channel_state_number()
1197                .unwrap(),
1198            None
1199        );
1200        assert_eq!(
1201            event
1202                .call_state()
1203                .unwrap(),
1204            None
1205        );
1206        assert_eq!(
1207            event
1208                .answer_state()
1209                .unwrap(),
1210            None
1211        );
1212        assert_eq!(
1213            event
1214                .call_direction()
1215                .unwrap(),
1216            None
1217        );
1218    }
1219
1220    // --- Repeating SIP header tests ---
1221
1222    #[test]
1223    fn test_sip_p_asserted_identity_comma_separated() {
1224        let mut event = EslEvent::new();
1225        // RFC 3325: P-Asserted-Identity can carry two identities (one sip:, one tel:)
1226        // FreeSWITCH stores the comma-separated value as a single channel variable
1227        event.set_header(
1228            "variable_sip_P-Asserted-Identity",
1229            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1230        );
1231
1232        assert_eq!(
1233            event.variable_str("sip_P-Asserted-Identity"),
1234            Some("<sip:alice@atlanta.example.com>, <tel:+15551234567>")
1235        );
1236    }
1237
1238    #[test]
1239    fn test_sip_p_asserted_identity_array_format() {
1240        let mut event = EslEvent::new();
1241        // When FreeSWITCH stores repeated SIP headers via ARRAY format
1242        event.push_header(
1243            "variable_sip_P-Asserted-Identity",
1244            "<sip:alice@atlanta.example.com>",
1245        );
1246        event.push_header("variable_sip_P-Asserted-Identity", "<tel:+15551234567>");
1247
1248        let raw = event
1249            .header_str("variable_sip_P-Asserted-Identity")
1250            .unwrap();
1251        assert_eq!(
1252            raw,
1253            "ARRAY::<sip:alice@atlanta.example.com>|:<tel:+15551234567>"
1254        );
1255
1256        let arr = crate::variables::EslArray::parse(raw).unwrap();
1257        assert_eq!(arr.len(), 2);
1258        assert_eq!(arr.items()[0], "<sip:alice@atlanta.example.com>");
1259        assert_eq!(arr.items()[1], "<tel:+15551234567>");
1260    }
1261
1262    #[test]
1263    fn test_sip_header_with_colons_in_uri() {
1264        let mut event = EslEvent::new();
1265        // SIP URIs contain colons (sip:, sips:) which must not confuse ARRAY parsing
1266        event.push_header(
1267            "variable_sip_h_Diversion",
1268            "<sip:+15551234567@gw.example.com;reason=unconditional>",
1269        );
1270        event.push_header(
1271            "variable_sip_h_Diversion",
1272            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>",
1273        );
1274
1275        let raw = event
1276            .header_str("variable_sip_h_Diversion")
1277            .unwrap();
1278        let arr = crate::variables::EslArray::parse(raw).unwrap();
1279        assert_eq!(arr.len(), 2);
1280        assert_eq!(
1281            arr.items()[0],
1282            "<sip:+15551234567@gw.example.com;reason=unconditional>"
1283        );
1284        assert_eq!(
1285            arr.items()[1],
1286            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>"
1287        );
1288    }
1289
1290    #[test]
1291    fn test_sip_p_asserted_identity_plain_format_round_trip() {
1292        let mut event = EslEvent::with_type(EslEventType::ChannelCreate);
1293        event.set_header("Event-Name", "CHANNEL_CREATE");
1294        event.set_header(
1295            "variable_sip_P-Asserted-Identity",
1296            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
1297        );
1298
1299        let plain = event.to_plain_format();
1300        // The comma-separated value should be percent-encoded on the wire
1301        assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
1302        // Angle brackets and comma should be encoded
1303        assert!(!plain.contains("<sip:alice"));
1304    }
1305
1306    // --- Header key normalization on EslEvent ---
1307    // set_header() normalizes keys so lookups via header(EventHeader::X)
1308    // and header_str() work regardless of the casing used at insertion.
1309
1310    #[test]
1311    fn set_header_normalizes_known_enum_variant() {
1312        let mut event = EslEvent::new();
1313        event.set_header("unique-id", "abc-123");
1314        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1315    }
1316
1317    #[test]
1318    fn set_header_normalizes_codec_header() {
1319        let mut event = EslEvent::new();
1320        event.set_header("channel-read-codec-bit-rate", "128000");
1321        assert_eq!(
1322            event.header(EventHeader::ChannelReadCodecBitRate),
1323            Some("128000")
1324        );
1325    }
1326
1327    #[test]
1328    fn header_str_finds_by_original_key() {
1329        let mut event = EslEvent::new();
1330        event.set_header("unique-id", "abc-123");
1331        // Lookup by original non-canonical key should still work
1332        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1333        // Lookup by canonical key also works
1334        assert_eq!(event.header_str("Unique-ID"), Some("abc-123"));
1335    }
1336
1337    #[test]
1338    fn header_str_finds_unknown_dash_header_by_original() {
1339        let mut event = EslEvent::new();
1340        event.set_header("x-custom-header", "val");
1341        // Stored as Title-Case
1342        assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
1343        // Original key also works via alias
1344        assert_eq!(event.header_str("x-custom-header"), Some("val"));
1345    }
1346
1347    #[test]
1348    fn set_header_underscore_passthrough_preserves_sip_h() {
1349        let mut event = EslEvent::new();
1350        event.set_header("variable_sip_h_X-My-CUSTOM-Header", "val");
1351        assert_eq!(
1352            event.header_str("variable_sip_h_X-My-CUSTOM-Header"),
1353            Some("val")
1354        );
1355    }
1356
1357    #[test]
1358    fn set_header_different_casing_overwrites() {
1359        let mut event = EslEvent::new();
1360        event.set_header("Unique-ID", "first");
1361        event.set_header("unique-id", "second");
1362        // Both normalize to "Unique-ID", second overwrites first
1363        assert_eq!(event.header(EventHeader::UniqueId), Some("second"));
1364    }
1365
1366    #[test]
1367    fn remove_header_by_original_key() {
1368        let mut event = EslEvent::new();
1369        event.set_header("unique-id", "abc-123");
1370        let removed = event.remove_header("unique-id");
1371        assert_eq!(removed, Some("abc-123".to_string()));
1372        assert_eq!(event.header(EventHeader::UniqueId), None);
1373    }
1374
1375    #[test]
1376    fn remove_header_by_canonical_key() {
1377        let mut event = EslEvent::new();
1378        event.set_header("unique-id", "abc-123");
1379        let removed = event.remove_header("Unique-ID");
1380        assert_eq!(removed, Some("abc-123".to_string()));
1381        assert_eq!(event.header_str("unique-id"), None);
1382    }
1383
1384    #[test]
1385    fn serde_round_trip_preserves_canonical_lookups() {
1386        let mut event = EslEvent::new();
1387        event.set_header("unique-id", "abc-123");
1388        event.set_header("channel-read-codec-bit-rate", "128000");
1389        let json = serde_json::to_string(&event).unwrap();
1390        let deserialized: EslEvent = serde_json::from_str(&json).unwrap();
1391        assert_eq!(deserialized.header(EventHeader::UniqueId), Some("abc-123"));
1392        assert_eq!(
1393            deserialized.header(EventHeader::ChannelReadCodecBitRate),
1394            Some("128000")
1395        );
1396    }
1397
1398    #[test]
1399    fn serde_deserialize_normalizes_external_json() {
1400        let json = r#"{"event_type":null,"headers":{"unique-id":"abc-123","channel-read-codec-bit-rate":"128000"},"body":null}"#;
1401        let event: EslEvent = serde_json::from_str(json).unwrap();
1402        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
1403        assert_eq!(
1404            event.header(EventHeader::ChannelReadCodecBitRate),
1405            Some("128000")
1406        );
1407        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
1408    }
1409
1410    #[test]
1411    fn test_event_typed_accessors_invalid_values() {
1412        let mut event = EslEvent::new();
1413        event.set_header("Channel-State", "BOGUS");
1414        event.set_header("Channel-State-Number", "999");
1415        event.set_header("Channel-Call-State", "BOGUS");
1416        event.set_header("Answer-State", "bogus");
1417        event.set_header("Call-Direction", "bogus");
1418        assert!(event
1419            .channel_state()
1420            .is_err());
1421        assert!(event
1422            .channel_state_number()
1423            .is_err());
1424        assert!(event
1425            .call_state()
1426            .is_err());
1427        assert!(event
1428            .answer_state()
1429            .is_err());
1430        assert!(event
1431            .call_direction()
1432            .is_err());
1433    }
1434}