Skip to main content

freeswitch_types/
event.rs

1//! ESL event types and structures
2
3use crate::headers::EventHeader;
4use crate::lookup::HeaderLookup;
5use crate::variables::EslArray;
6use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fmt;
10use std::str::FromStr;
11
12/// Event format types supported by FreeSWITCH ESL
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, 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, Serialize, Deserialize)]
96        #[non_exhaustive]
97        #[allow(missing_docs)]
98        pub enum EslEventType {
99            $(
100                $(#[$attr])*
101                $variant,
102            )+
103            $(
104                $(#[$extra_attr])*
105                $extra_variant,
106            )*
107        }
108
109        impl fmt::Display for EslEventType {
110            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111                f.write_str(self.as_str())
112            }
113        }
114
115        impl EslEventType {
116            /// Returns the canonical wire name as a static string slice.
117            pub const fn as_str(&self) -> &'static str {
118                match self {
119                    $( EslEventType::$variant => $wire, )+
120                    $( EslEventType::$extra_variant => $extra_wire, )*
121                }
122            }
123
124            /// Parse event type from wire name (canonical case).
125            pub fn parse_event_type(s: &str) -> Option<Self> {
126                match s {
127                    $( $wire => Some(EslEventType::$variant), )+
128                    $( $extra_wire => Some(EslEventType::$extra_variant), )*
129                    _ => None,
130                }
131            }
132        }
133
134        impl FromStr for EslEventType {
135            type Err = ParseEventTypeError;
136
137            fn from_str(s: &str) -> Result<Self, Self::Err> {
138                Self::parse_event_type(s).ok_or_else(|| ParseEventTypeError(s.to_string()))
139            }
140        }
141    };
142}
143
144esl_event_types! {
145    Custom => "CUSTOM",
146    Clone => "CLONE",
147    ChannelCreate => "CHANNEL_CREATE",
148    ChannelDestroy => "CHANNEL_DESTROY",
149    ChannelState => "CHANNEL_STATE",
150    ChannelCallstate => "CHANNEL_CALLSTATE",
151    ChannelAnswer => "CHANNEL_ANSWER",
152    ChannelHangup => "CHANNEL_HANGUP",
153    ChannelHangupComplete => "CHANNEL_HANGUP_COMPLETE",
154    ChannelExecute => "CHANNEL_EXECUTE",
155    ChannelExecuteComplete => "CHANNEL_EXECUTE_COMPLETE",
156    ChannelHold => "CHANNEL_HOLD",
157    ChannelUnhold => "CHANNEL_UNHOLD",
158    ChannelBridge => "CHANNEL_BRIDGE",
159    ChannelUnbridge => "CHANNEL_UNBRIDGE",
160    ChannelProgress => "CHANNEL_PROGRESS",
161    ChannelProgressMedia => "CHANNEL_PROGRESS_MEDIA",
162    ChannelOutgoing => "CHANNEL_OUTGOING",
163    ChannelPark => "CHANNEL_PARK",
164    ChannelUnpark => "CHANNEL_UNPARK",
165    ChannelApplication => "CHANNEL_APPLICATION",
166    ChannelOriginate => "CHANNEL_ORIGINATE",
167    ChannelUuid => "CHANNEL_UUID",
168    Api => "API",
169    Log => "LOG",
170    InboundChan => "INBOUND_CHAN",
171    OutboundChan => "OUTBOUND_CHAN",
172    Startup => "STARTUP",
173    Shutdown => "SHUTDOWN",
174    Publish => "PUBLISH",
175    Unpublish => "UNPUBLISH",
176    Talk => "TALK",
177    Notalk => "NOTALK",
178    SessionCrash => "SESSION_CRASH",
179    ModuleLoad => "MODULE_LOAD",
180    ModuleUnload => "MODULE_UNLOAD",
181    Dtmf => "DTMF",
182    Message => "MESSAGE",
183    PresenceIn => "PRESENCE_IN",
184    NotifyIn => "NOTIFY_IN",
185    PresenceOut => "PRESENCE_OUT",
186    PresenceProbe => "PRESENCE_PROBE",
187    MessageWaiting => "MESSAGE_WAITING",
188    MessageQuery => "MESSAGE_QUERY",
189    Roster => "ROSTER",
190    Codec => "CODEC",
191    BackgroundJob => "BACKGROUND_JOB",
192    DetectedSpeech => "DETECTED_SPEECH",
193    DetectedTone => "DETECTED_TONE",
194    PrivateCommand => "PRIVATE_COMMAND",
195    Heartbeat => "HEARTBEAT",
196    Trap => "TRAP",
197    AddSchedule => "ADD_SCHEDULE",
198    DelSchedule => "DEL_SCHEDULE",
199    ExeSchedule => "EXE_SCHEDULE",
200    ReSchedule => "RE_SCHEDULE",
201    ReloadXml => "RELOADXML",
202    Notify => "NOTIFY",
203    PhoneFeature => "PHONE_FEATURE",
204    PhoneFeatureSubscribe => "PHONE_FEATURE_SUBSCRIBE",
205    SendMessage => "SEND_MESSAGE",
206    RecvMessage => "RECV_MESSAGE",
207    RequestParams => "REQUEST_PARAMS",
208    ChannelData => "CHANNEL_DATA",
209    General => "GENERAL",
210    Command => "COMMAND",
211    SessionHeartbeat => "SESSION_HEARTBEAT",
212    ClientDisconnected => "CLIENT_DISCONNECTED",
213    ServerDisconnected => "SERVER_DISCONNECTED",
214    SendInfo => "SEND_INFO",
215    RecvInfo => "RECV_INFO",
216    RecvRtcpMessage => "RECV_RTCP_MESSAGE",
217    SendRtcpMessage => "SEND_RTCP_MESSAGE",
218    CallSecure => "CALL_SECURE",
219    Nat => "NAT",
220    RecordStart => "RECORD_START",
221    RecordStop => "RECORD_STOP",
222    PlaybackStart => "PLAYBACK_START",
223    PlaybackStop => "PLAYBACK_STOP",
224    CallUpdate => "CALL_UPDATE",
225    Failure => "FAILURE",
226    SocketData => "SOCKET_DATA",
227    MediaBugStart => "MEDIA_BUG_START",
228    MediaBugStop => "MEDIA_BUG_STOP",
229    ConferenceDataQuery => "CONFERENCE_DATA_QUERY",
230    ConferenceData => "CONFERENCE_DATA",
231    CallSetupReq => "CALL_SETUP_REQ",
232    CallSetupResult => "CALL_SETUP_RESULT",
233    CallDetail => "CALL_DETAIL",
234    DeviceState => "DEVICE_STATE",
235    Text => "TEXT",
236    ShutdownRequested => "SHUTDOWN_REQUESTED",
237    /// Subscribe to all events
238    All => "ALL";
239    // --- Not in libs/esl/ EVENT_NAMES[], only in switch_event.c ---
240    // check-event-types.sh stops scanning at the All variant above.
241    /// Present in `switch_event.c` but not in `libs/esl/` EVENT_NAMES[].
242    StartRecording => "START_RECORDING",
243}
244
245// -- Event group constants --------------------------------------------------
246//
247// Predefined slices for common subscription patterns. Pass directly to
248// `EslClient::subscribe_events()`.
249//
250// MAINTENANCE: when adding new `EslEventType` variants, check whether they
251// belong in any of these groups and update accordingly.
252
253impl EslEventType {
254    /// Every `CHANNEL_*` event type.
255    ///
256    /// Covers the full channel lifecycle: creation, state changes, execution,
257    /// bridging, hold, park, progress, originate, and destruction.
258    ///
259    /// ```rust
260    /// use freeswitch_types::EslEventType;
261    /// assert!(EslEventType::CHANNEL_EVENTS.contains(&EslEventType::ChannelCreate));
262    /// assert!(EslEventType::CHANNEL_EVENTS.contains(&EslEventType::ChannelHangupComplete));
263    /// ```
264    pub const CHANNEL_EVENTS: &[EslEventType] = &[
265        EslEventType::ChannelCreate,
266        EslEventType::ChannelDestroy,
267        EslEventType::ChannelState,
268        EslEventType::ChannelCallstate,
269        EslEventType::ChannelAnswer,
270        EslEventType::ChannelHangup,
271        EslEventType::ChannelHangupComplete,
272        EslEventType::ChannelExecute,
273        EslEventType::ChannelExecuteComplete,
274        EslEventType::ChannelHold,
275        EslEventType::ChannelUnhold,
276        EslEventType::ChannelBridge,
277        EslEventType::ChannelUnbridge,
278        EslEventType::ChannelProgress,
279        EslEventType::ChannelProgressMedia,
280        EslEventType::ChannelOutgoing,
281        EslEventType::ChannelPark,
282        EslEventType::ChannelUnpark,
283        EslEventType::ChannelApplication,
284        EslEventType::ChannelOriginate,
285        EslEventType::ChannelUuid,
286        EslEventType::ChannelData,
287    ];
288
289    /// In-call events: DTMF, VAD speech detection, media security, and call updates.
290    ///
291    /// Events that fire during an established call, tied to RTP/media activity
292    /// rather than signaling state transitions.
293    ///
294    /// ```rust
295    /// use freeswitch_types::EslEventType;
296    /// assert!(EslEventType::IN_CALL_EVENTS.contains(&EslEventType::Dtmf));
297    /// assert!(EslEventType::IN_CALL_EVENTS.contains(&EslEventType::Talk));
298    /// ```
299    pub const IN_CALL_EVENTS: &[EslEventType] = &[
300        EslEventType::Dtmf,
301        EslEventType::Talk,
302        EslEventType::Notalk,
303        EslEventType::CallSecure,
304        EslEventType::CallUpdate,
305        EslEventType::RecvRtcpMessage,
306        EslEventType::SendRtcpMessage,
307    ];
308
309    /// Media-related events: playback, recording, media bugs, and detection.
310    ///
311    /// Useful for IVR applications that need to track media operations without
312    /// subscribing to the full channel lifecycle.
313    ///
314    /// ```rust
315    /// use freeswitch_types::EslEventType;
316    /// assert!(EslEventType::MEDIA_EVENTS.contains(&EslEventType::PlaybackStart));
317    /// assert!(EslEventType::MEDIA_EVENTS.contains(&EslEventType::DetectedSpeech));
318    /// ```
319    pub const MEDIA_EVENTS: &[EslEventType] = &[
320        EslEventType::PlaybackStart,
321        EslEventType::PlaybackStop,
322        EslEventType::RecordStart,
323        EslEventType::RecordStop,
324        EslEventType::StartRecording,
325        EslEventType::MediaBugStart,
326        EslEventType::MediaBugStop,
327        EslEventType::DetectedSpeech,
328        EslEventType::DetectedTone,
329    ];
330
331    /// Presence and messaging events.
332    ///
333    /// For applications that track user presence (BLF, buddy lists) or
334    /// message-waiting indicators (voicemail MWI).
335    ///
336    /// ```rust
337    /// use freeswitch_types::EslEventType;
338    /// assert!(EslEventType::PRESENCE_EVENTS.contains(&EslEventType::PresenceIn));
339    /// assert!(EslEventType::PRESENCE_EVENTS.contains(&EslEventType::MessageWaiting));
340    /// ```
341    pub const PRESENCE_EVENTS: &[EslEventType] = &[
342        EslEventType::PresenceIn,
343        EslEventType::PresenceOut,
344        EslEventType::PresenceProbe,
345        EslEventType::MessageWaiting,
346        EslEventType::MessageQuery,
347        EslEventType::Roster,
348    ];
349
350    /// System lifecycle events.
351    ///
352    /// Server startup/shutdown, heartbeats, module loading, and XML reloads.
353    /// Useful for monitoring dashboards and operational tooling.
354    ///
355    /// ```rust
356    /// use freeswitch_types::EslEventType;
357    /// assert!(EslEventType::SYSTEM_EVENTS.contains(&EslEventType::Heartbeat));
358    /// assert!(EslEventType::SYSTEM_EVENTS.contains(&EslEventType::Shutdown));
359    /// ```
360    pub const SYSTEM_EVENTS: &[EslEventType] = &[
361        EslEventType::Startup,
362        EslEventType::Shutdown,
363        EslEventType::ShutdownRequested,
364        EslEventType::Heartbeat,
365        EslEventType::SessionHeartbeat,
366        EslEventType::SessionCrash,
367        EslEventType::ModuleLoad,
368        EslEventType::ModuleUnload,
369        EslEventType::ReloadXml,
370    ];
371
372    /// Conference-related events.
373    ///
374    /// ```rust
375    /// use freeswitch_types::EslEventType;
376    /// assert!(EslEventType::CONFERENCE_EVENTS.contains(&EslEventType::ConferenceData));
377    /// ```
378    pub const CONFERENCE_EVENTS: &[EslEventType] = &[
379        EslEventType::ConferenceDataQuery,
380        EslEventType::ConferenceData,
381    ];
382}
383
384/// Error returned when parsing an unknown event type string.
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct ParseEventTypeError(pub String);
387
388impl fmt::Display for ParseEventTypeError {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        write!(f, "unknown event type: {}", self.0)
391    }
392}
393
394impl std::error::Error for ParseEventTypeError {}
395
396/// Event priority levels matching FreeSWITCH `esl_priority_t`
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
398#[non_exhaustive]
399pub enum EslEventPriority {
400    /// Default priority.
401    Normal,
402    /// Lower than normal.
403    Low,
404    /// Higher than normal.
405    High,
406}
407
408impl fmt::Display for EslEventPriority {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self {
411            EslEventPriority::Normal => write!(f, "NORMAL"),
412            EslEventPriority::Low => write!(f, "LOW"),
413            EslEventPriority::High => write!(f, "HIGH"),
414        }
415    }
416}
417
418/// Error returned when parsing an invalid priority string.
419#[derive(Debug, Clone, PartialEq, Eq)]
420pub struct ParsePriorityError(pub String);
421
422impl fmt::Display for ParsePriorityError {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        write!(f, "unknown priority: {}", self.0)
425    }
426}
427
428impl std::error::Error for ParsePriorityError {}
429
430impl FromStr for EslEventPriority {
431    type Err = ParsePriorityError;
432
433    fn from_str(s: &str) -> Result<Self, Self::Err> {
434        match s {
435            "NORMAL" => Ok(EslEventPriority::Normal),
436            "LOW" => Ok(EslEventPriority::Low),
437            "HIGH" => Ok(EslEventPriority::High),
438            _ => Err(ParsePriorityError(s.to_string())),
439        }
440    }
441}
442
443/// ESL Event structure containing headers and optional body
444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
445pub struct EslEvent {
446    event_type: Option<EslEventType>,
447    headers: HashMap<String, String>,
448    body: Option<String>,
449}
450
451impl EslEvent {
452    /// Create a new empty event
453    pub fn new() -> Self {
454        Self {
455            event_type: None,
456            headers: HashMap::new(),
457            body: None,
458        }
459    }
460
461    /// Create event with specified type
462    pub fn with_type(event_type: EslEventType) -> Self {
463        Self {
464            event_type: Some(event_type),
465            headers: HashMap::new(),
466            body: None,
467        }
468    }
469
470    /// Parsed event type, if recognized.
471    pub fn event_type(&self) -> Option<EslEventType> {
472        self.event_type
473    }
474
475    /// Override the event type.
476    pub fn set_event_type(&mut self, event_type: Option<EslEventType>) {
477        self.event_type = event_type;
478    }
479
480    /// Look up a header by its [`EventHeader`] enum variant (case-sensitive).
481    ///
482    /// For headers not covered by `EventHeader`, use [`header_str()`](Self::header_str).
483    pub fn header(&self, name: EventHeader) -> Option<&str> {
484        self.headers
485            .get(name.as_str())
486            .map(|s| s.as_str())
487    }
488
489    /// Look up a header by its raw wire name (case-sensitive).
490    ///
491    /// Use [`header()`](Self::header) with an [`EventHeader`] variant for known
492    /// headers. This method is for headers not (yet) covered by the enum,
493    /// such as custom `X-` headers or FreeSWITCH headers added after this
494    /// library was published.
495    pub fn header_str(&self, name: &str) -> Option<&str> {
496        self.headers
497            .get(name)
498            .map(|s| s.as_str())
499    }
500
501    /// Look up a channel variable by its bare name.
502    ///
503    /// Equivalent to [`variable()`](Self::variable) but matches the
504    /// [`HeaderLookup`] trait signature.
505    pub fn variable_str(&self, name: &str) -> Option<&str> {
506        let key = format!("variable_{}", name);
507        self.header_str(&key)
508    }
509
510    /// All headers as a map.
511    pub fn headers(&self) -> &HashMap<String, String> {
512        &self.headers
513    }
514
515    /// Set or overwrite a header.
516    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
517        self.headers
518            .insert(name.into(), value.into());
519    }
520
521    /// Remove a header, returning its value if it existed.
522    pub fn remove_header(&mut self, name: impl AsRef<str>) -> Option<String> {
523        self.headers
524            .remove(name.as_ref())
525    }
526
527    /// Event body (the content after the blank line in plain-text events).
528    pub fn body(&self) -> Option<&str> {
529        self.body
530            .as_deref()
531    }
532
533    /// Set the event body.
534    pub fn set_body(&mut self, body: impl Into<String>) {
535        self.body = Some(body.into());
536    }
537
538    /// Sets the `priority` header carried on the event.
539    ///
540    /// FreeSWITCH stores this as metadata but does **not** use it for dispatch
541    /// ordering — all events are delivered FIFO regardless of priority.
542    pub fn set_priority(&mut self, priority: EslEventPriority) {
543        self.set_header(EventHeader::Priority.as_str(), priority.to_string());
544    }
545
546    /// Append a value to a multi-value header (PUSH semantics).
547    ///
548    /// If the header doesn't exist, sets it as a plain value.
549    /// If it exists as a plain value, converts to `ARRAY::old|:new`.
550    /// If it already has an `ARRAY::` prefix, appends the new value.
551    ///
552    /// ```
553    /// # use freeswitch_types::EslEvent;
554    /// let mut event = EslEvent::new();
555    /// event.push_header("X-Test", "first");
556    /// event.push_header("X-Test", "second");
557    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
558    /// ```
559    pub fn push_header(&mut self, name: &str, value: &str) {
560        self.stack_header(name, value, EslArray::push);
561    }
562
563    /// Prepend a value to a multi-value header (UNSHIFT semantics).
564    ///
565    /// Same conversion rules as `push_header()`, but inserts at the front.
566    ///
567    /// ```
568    /// # use freeswitch_types::EslEvent;
569    /// let mut event = EslEvent::new();
570    /// event.set_header("X-Test", "ARRAY::b|:c");
571    /// event.unshift_header("X-Test", "a");
572    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
573    /// ```
574    pub fn unshift_header(&mut self, name: &str, value: &str) {
575        self.stack_header(name, value, EslArray::unshift);
576    }
577
578    fn stack_header(&mut self, name: &str, value: &str, op: fn(&mut EslArray, String)) {
579        match self
580            .headers
581            .get(name)
582        {
583            None => {
584                self.set_header(name, value);
585            }
586            Some(existing) => {
587                let mut arr = match EslArray::parse(existing) {
588                    Some(arr) => arr,
589                    None => EslArray::new(vec![existing.clone()]),
590                };
591                op(&mut arr, value.into());
592                self.set_header(name, arr.to_string());
593            }
594        }
595    }
596
597    /// Check whether this event matches the given type.
598    pub fn is_event_type(&self, event_type: EslEventType) -> bool {
599        self.event_type == Some(event_type)
600    }
601
602    /// Serialize to ESL plain text wire format with percent-encoded header values.
603    ///
604    /// This is the inverse of `EslParser::parse_plain_event()`. The output can
605    /// be fed back through the parser to reconstruct an equivalent `EslEvent`
606    /// (round-trip).
607    ///
608    /// `Event-Name` is emitted first, remaining headers are sorted alphabetically
609    /// for deterministic output. `Content-Length` from stored headers is skipped
610    /// and recomputed from the body if present.
611    pub fn to_plain_format(&self) -> String {
612        use std::fmt::Write;
613        let mut result = String::new();
614
615        let event_name_key = EventHeader::EventName.as_str();
616        if let Some(event_name) = self
617            .headers
618            .get(event_name_key)
619        {
620            let _ = writeln!(
621                result,
622                "{}: {}",
623                event_name_key,
624                percent_encode(event_name.as_bytes(), NON_ALPHANUMERIC)
625            );
626        }
627
628        let mut sorted_headers: Vec<_> = self
629            .headers
630            .iter()
631            .filter(|(k, _)| k.as_str() != event_name_key && k.as_str() != "Content-Length")
632            .collect();
633        sorted_headers.sort_by_key(|(k, _)| k.as_str());
634
635        for (key, value) in sorted_headers {
636            let _ = writeln!(
637                result,
638                "{}: {}",
639                key,
640                percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
641            );
642        }
643
644        if let Some(body) = &self.body {
645            let _ = writeln!(result, "Content-Length: {}", body.len());
646            result.push('\n');
647            result.push_str(body);
648        } else {
649            result.push('\n');
650        }
651
652        result
653    }
654}
655
656impl Default for EslEvent {
657    fn default() -> Self {
658        Self::new()
659    }
660}
661
662impl HeaderLookup for EslEvent {
663    fn header_str(&self, name: &str) -> Option<&str> {
664        self.headers
665            .get(name)
666            .map(|s| s.as_str())
667    }
668
669    fn variable_str(&self, name: &str) -> Option<&str> {
670        let key = format!("variable_{}", name);
671        self.header_str(&key)
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678
679    #[test]
680    fn test_notify_in_parse() {
681        assert_eq!(
682            EslEventType::parse_event_type("NOTIFY_IN"),
683            Some(EslEventType::NotifyIn)
684        );
685        assert_eq!(EslEventType::parse_event_type("notify_in"), None);
686    }
687
688    #[test]
689    fn test_notify_in_display() {
690        assert_eq!(EslEventType::NotifyIn.to_string(), "NOTIFY_IN");
691    }
692
693    #[test]
694    fn test_notify_in_distinct_from_notify() {
695        assert_ne!(EslEventType::Notify, EslEventType::NotifyIn);
696        assert_ne!(
697            EslEventType::Notify.to_string(),
698            EslEventType::NotifyIn.to_string()
699        );
700    }
701
702    #[test]
703    fn test_wire_names_match_c_esl() {
704        assert_eq!(
705            EslEventType::ChannelOutgoing.to_string(),
706            "CHANNEL_OUTGOING"
707        );
708        assert_eq!(EslEventType::Api.to_string(), "API");
709        assert_eq!(EslEventType::ReloadXml.to_string(), "RELOADXML");
710        assert_eq!(EslEventType::PresenceIn.to_string(), "PRESENCE_IN");
711        assert_eq!(EslEventType::Roster.to_string(), "ROSTER");
712        assert_eq!(EslEventType::Text.to_string(), "TEXT");
713        assert_eq!(EslEventType::ReSchedule.to_string(), "RE_SCHEDULE");
714
715        assert_eq!(
716            EslEventType::parse_event_type("CHANNEL_OUTGOING"),
717            Some(EslEventType::ChannelOutgoing)
718        );
719        assert_eq!(
720            EslEventType::parse_event_type("API"),
721            Some(EslEventType::Api)
722        );
723        assert_eq!(
724            EslEventType::parse_event_type("RELOADXML"),
725            Some(EslEventType::ReloadXml)
726        );
727        assert_eq!(
728            EslEventType::parse_event_type("PRESENCE_IN"),
729            Some(EslEventType::PresenceIn)
730        );
731    }
732
733    #[test]
734    fn test_event_type_from_str() {
735        assert_eq!(
736            "CHANNEL_ANSWER".parse::<EslEventType>(),
737            Ok(EslEventType::ChannelAnswer)
738        );
739        assert!("channel_answer"
740            .parse::<EslEventType>()
741            .is_err());
742        assert!("UNKNOWN_EVENT"
743            .parse::<EslEventType>()
744            .is_err());
745    }
746
747    #[test]
748    fn test_remove_header() {
749        let mut event = EslEvent::new();
750        event.set_header("Foo", "bar");
751        event.set_header("Baz", "qux");
752
753        let removed = event.remove_header("Foo");
754        assert_eq!(removed, Some("bar".to_string()));
755        assert!(event
756            .header_str("Foo")
757            .is_none());
758        assert_eq!(event.header_str("Baz"), Some("qux"));
759
760        let removed_again = event.remove_header("Foo");
761        assert_eq!(removed_again, None);
762    }
763
764    #[test]
765    fn test_to_plain_format_basic() {
766        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
767        event.set_header("Event-Name", "HEARTBEAT");
768        event.set_header("Core-UUID", "abc-123");
769
770        let plain = event.to_plain_format();
771
772        assert!(plain.starts_with("Event-Name: "));
773        assert!(plain.contains("Core-UUID: "));
774        assert!(plain.ends_with("\n\n"));
775    }
776
777    #[test]
778    fn test_to_plain_format_percent_encoding() {
779        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
780        event.set_header("Event-Name", "HEARTBEAT");
781        event.set_header("Up-Time", "0 years, 0 days");
782
783        let plain = event.to_plain_format();
784
785        assert!(!plain.contains("0 years, 0 days"));
786        assert!(plain.contains("Up-Time: "));
787        assert!(plain.contains("%20"));
788    }
789
790    #[test]
791    fn test_to_plain_format_with_body() {
792        let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
793        event.set_header("Event-Name", "BACKGROUND_JOB");
794        event.set_header("Job-UUID", "def-456");
795        event.set_body("+OK result\n".to_string());
796
797        let plain = event.to_plain_format();
798
799        assert!(plain.contains("Content-Length: 11\n"));
800        assert!(plain.ends_with("\n\n+OK result\n"));
801    }
802
803    #[test]
804    fn test_set_priority_normal() {
805        let mut event = EslEvent::new();
806        event.set_priority(EslEventPriority::Normal);
807        assert_eq!(
808            event
809                .priority()
810                .unwrap(),
811            Some(EslEventPriority::Normal)
812        );
813        assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
814    }
815
816    #[test]
817    fn test_set_priority_high() {
818        let mut event = EslEvent::new();
819        event.set_priority(EslEventPriority::High);
820        assert_eq!(
821            event
822                .priority()
823                .unwrap(),
824            Some(EslEventPriority::High)
825        );
826        assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
827    }
828
829    #[test]
830    fn test_priority_display() {
831        assert_eq!(EslEventPriority::Normal.to_string(), "NORMAL");
832        assert_eq!(EslEventPriority::Low.to_string(), "LOW");
833        assert_eq!(EslEventPriority::High.to_string(), "HIGH");
834    }
835
836    #[test]
837    fn test_priority_from_str() {
838        assert_eq!(
839            "NORMAL".parse::<EslEventPriority>(),
840            Ok(EslEventPriority::Normal)
841        );
842        assert_eq!("LOW".parse::<EslEventPriority>(), Ok(EslEventPriority::Low));
843        assert_eq!(
844            "HIGH".parse::<EslEventPriority>(),
845            Ok(EslEventPriority::High)
846        );
847        assert!("INVALID"
848            .parse::<EslEventPriority>()
849            .is_err());
850    }
851
852    #[test]
853    fn test_priority_from_str_rejects_wrong_case() {
854        assert!("normal"
855            .parse::<EslEventPriority>()
856            .is_err());
857        assert!("Low"
858            .parse::<EslEventPriority>()
859            .is_err());
860        assert!("hIgH"
861            .parse::<EslEventPriority>()
862            .is_err());
863    }
864
865    #[test]
866    fn test_push_header_new() {
867        let mut event = EslEvent::new();
868        event.push_header("X-Test", "first");
869        assert_eq!(event.header_str("X-Test"), Some("first"));
870    }
871
872    #[test]
873    fn test_push_header_existing_plain() {
874        let mut event = EslEvent::new();
875        event.set_header("X-Test", "first");
876        event.push_header("X-Test", "second");
877        assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
878    }
879
880    #[test]
881    fn test_push_header_existing_array() {
882        let mut event = EslEvent::new();
883        event.set_header("X-Test", "ARRAY::a|:b");
884        event.push_header("X-Test", "c");
885        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
886    }
887
888    #[test]
889    fn test_unshift_header_new() {
890        let mut event = EslEvent::new();
891        event.unshift_header("X-Test", "only");
892        assert_eq!(event.header_str("X-Test"), Some("only"));
893    }
894
895    #[test]
896    fn test_unshift_header_existing_array() {
897        let mut event = EslEvent::new();
898        event.set_header("X-Test", "ARRAY::b|:c");
899        event.unshift_header("X-Test", "a");
900        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
901    }
902
903    #[test]
904    fn test_sendevent_with_priority_wire_format() {
905        let mut event = EslEvent::with_type(EslEventType::Custom);
906        event.set_header("Event-Name", "CUSTOM");
907        event.set_header("Event-Subclass", "test::priority");
908        event.set_priority(EslEventPriority::High);
909
910        let plain = event.to_plain_format();
911        assert!(plain.contains("priority: HIGH\n"));
912    }
913
914    #[test]
915    fn test_convenience_accessors() {
916        let mut event = EslEvent::new();
917        event.set_header("Channel-Name", "sofia/internal/1000@example.com");
918        event.set_header("Caller-Caller-ID-Number", "1000");
919        event.set_header("Caller-Caller-ID-Name", "Alice");
920        event.set_header("Hangup-Cause", "NORMAL_CLEARING");
921        event.set_header("Event-Subclass", "sofia::register");
922        event.set_header("variable_sip_from_display", "Bob");
923
924        assert_eq!(
925            event.channel_name(),
926            Some("sofia/internal/1000@example.com")
927        );
928        assert_eq!(event.caller_id_number(), Some("1000"));
929        assert_eq!(event.caller_id_name(), Some("Alice"));
930        assert_eq!(
931            event
932                .hangup_cause()
933                .unwrap(),
934            Some(crate::channel::HangupCause::NormalClearing)
935        );
936        assert_eq!(event.event_subclass(), Some("sofia::register"));
937        assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
938        assert_eq!(event.variable_str("nonexistent"), None);
939    }
940
941    #[test]
942    fn test_event_format_from_str() {
943        assert_eq!("plain".parse::<EventFormat>(), Ok(EventFormat::Plain));
944        assert_eq!("json".parse::<EventFormat>(), Ok(EventFormat::Json));
945        assert_eq!("xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
946        assert!("foo"
947            .parse::<EventFormat>()
948            .is_err());
949    }
950
951    #[test]
952    fn test_event_format_from_str_case_insensitive() {
953        assert_eq!("PLAIN".parse::<EventFormat>(), Ok(EventFormat::Plain));
954        assert_eq!("Json".parse::<EventFormat>(), Ok(EventFormat::Json));
955        assert_eq!("XML".parse::<EventFormat>(), Ok(EventFormat::Xml));
956        assert_eq!("Xml".parse::<EventFormat>(), Ok(EventFormat::Xml));
957    }
958
959    #[test]
960    fn test_event_format_from_content_type() {
961        assert_eq!(
962            EventFormat::from_content_type("text/event-json"),
963            Ok(EventFormat::Json)
964        );
965        assert_eq!(
966            EventFormat::from_content_type("text/event-xml"),
967            Ok(EventFormat::Xml)
968        );
969        assert_eq!(
970            EventFormat::from_content_type("text/event-plain"),
971            Ok(EventFormat::Plain)
972        );
973        assert!(EventFormat::from_content_type("unknown").is_err());
974    }
975
976    // --- EslEvent accessor tests (via HeaderLookup trait) ---
977
978    #[test]
979    fn test_event_channel_state_accessor() {
980        use crate::channel::ChannelState;
981        let mut event = EslEvent::new();
982        event.set_header("Channel-State", "CS_EXECUTE");
983        assert_eq!(
984            event
985                .channel_state()
986                .unwrap(),
987            Some(ChannelState::CsExecute)
988        );
989    }
990
991    #[test]
992    fn test_event_channel_state_number_accessor() {
993        use crate::channel::ChannelState;
994        let mut event = EslEvent::new();
995        event.set_header("Channel-State-Number", "4");
996        assert_eq!(
997            event
998                .channel_state_number()
999                .unwrap(),
1000            Some(ChannelState::CsExecute)
1001        );
1002    }
1003
1004    #[test]
1005    fn test_event_call_state_accessor() {
1006        use crate::channel::CallState;
1007        let mut event = EslEvent::new();
1008        event.set_header("Channel-Call-State", "ACTIVE");
1009        assert_eq!(
1010            event
1011                .call_state()
1012                .unwrap(),
1013            Some(CallState::Active)
1014        );
1015    }
1016
1017    #[test]
1018    fn test_event_answer_state_accessor() {
1019        use crate::channel::AnswerState;
1020        let mut event = EslEvent::new();
1021        event.set_header("Answer-State", "answered");
1022        assert_eq!(
1023            event
1024                .answer_state()
1025                .unwrap(),
1026            Some(AnswerState::Answered)
1027        );
1028    }
1029
1030    #[test]
1031    fn test_event_call_direction_accessor() {
1032        use crate::channel::CallDirection;
1033        let mut event = EslEvent::new();
1034        event.set_header("Call-Direction", "inbound");
1035        assert_eq!(
1036            event
1037                .call_direction()
1038                .unwrap(),
1039            Some(CallDirection::Inbound)
1040        );
1041    }
1042
1043    #[test]
1044    fn test_event_typed_accessors_missing_headers() {
1045        let event = EslEvent::new();
1046        assert_eq!(
1047            event
1048                .channel_state()
1049                .unwrap(),
1050            None
1051        );
1052        assert_eq!(
1053            event
1054                .channel_state_number()
1055                .unwrap(),
1056            None
1057        );
1058        assert_eq!(
1059            event
1060                .call_state()
1061                .unwrap(),
1062            None
1063        );
1064        assert_eq!(
1065            event
1066                .answer_state()
1067                .unwrap(),
1068            None
1069        );
1070        assert_eq!(
1071            event
1072                .call_direction()
1073                .unwrap(),
1074            None
1075        );
1076    }
1077
1078    #[test]
1079    fn test_event_typed_accessors_invalid_values() {
1080        let mut event = EslEvent::new();
1081        event.set_header("Channel-State", "BOGUS");
1082        event.set_header("Channel-State-Number", "999");
1083        event.set_header("Channel-Call-State", "BOGUS");
1084        event.set_header("Answer-State", "bogus");
1085        event.set_header("Call-Direction", "bogus");
1086        assert!(event
1087            .channel_state()
1088            .is_err());
1089        assert!(event
1090            .channel_state_number()
1091            .is_err());
1092        assert!(event
1093            .call_state()
1094            .is_err());
1095        assert!(event
1096            .answer_state()
1097            .is_err());
1098        assert!(event
1099            .call_direction()
1100            .is_err());
1101    }
1102}