Skip to main content

freeswitch_types/event/
mod.rs

1//! ESL event types and structures
2
3mod event_type;
4mod format;
5mod subscription;
6
7pub use event_type::{EslEventType, ParseEventTypeError};
8pub use format::{EventFormat, ParseEventFormatError};
9pub use subscription::{
10    order_event_tokens, swallowed_event_types, EventSubscription, EventSubscriptionError,
11};
12
13use crate::headers::EventHeader;
14use crate::lookup::{variable_key, HeaderLookup};
15use crate::lossy_values::LossyValues;
16use crate::variables::{EslArray, EslArrayError, EslHeaders};
17use indexmap::IndexMap;
18use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
19use std::fmt;
20
21wire_enum! {
22    /// Event priority levels matching FreeSWITCH `esl_priority_t`
23    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24    pub enum EslEventPriority {
25        /// Default priority.
26        Normal => "NORMAL",
27        /// Lower than normal.
28        Low => "LOW",
29        /// Higher than normal.
30        High => "HIGH",
31    }
32    error ParsePriorityError("priority");
33    tests: esl_event_priority_wire_tests;
34}
35
36/// ESL Event structure containing headers and optional body
37#[derive(Debug, Clone, Eq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39pub struct EslEvent {
40    headers: EslHeaders,
41    body: Option<String>,
42    /// Exact wire bytes of a body that was not valid UTF-8; `body` then
43    /// holds the U+FFFD-substituted string. `None` in the normal case.
44    #[cfg_attr(
45        feature = "serde",
46        serde(default, skip_serializing_if = "Option::is_none")
47    )]
48    raw_body: Option<Vec<u8>>,
49    #[cfg_attr(
50        feature = "serde",
51        serde(default, skip_serializing_if = "LossyValues::is_empty")
52    )]
53    lossy_values: LossyValues,
54}
55
56impl EslEvent {
57    /// Create a new empty event
58    pub fn new() -> Self {
59        Self {
60            headers: EslHeaders::new(),
61            body: None,
62            raw_body: None,
63            lossy_values: LossyValues::default(),
64        }
65    }
66
67    /// Create event with the `Event-Name` header set to the given type's
68    /// wire name. The event type is derived lazily from this header on
69    /// every [`event_type()`](Self::event_type) call — there is no
70    /// separate `event_type` field.
71    pub fn with_type(event_type: EslEventType) -> Self {
72        let mut event = Self::new();
73        event.set_header(EventHeader::EventName.as_str(), event_type.as_str());
74        event
75    }
76
77    /// Parsed event type, derived from the `Event-Name` header.
78    ///
79    /// Returns `None` if the header is missing or carries a value that
80    /// is not a recognized [`EslEventType`] variant. Single source of
81    /// truth: the header. Mutating `Event-Name` via `set_header` will
82    /// be reflected on the next call.
83    pub fn event_type(&self) -> Option<EslEventType> {
84        self.header(EventHeader::EventName)
85            .and_then(EslEventType::parse_event_type)
86    }
87
88    /// Look up a header by its [`EventHeader`] enum variant.
89    ///
90    /// For headers not covered by `EventHeader`, use [`header_str()`](Self::header_str).
91    pub fn header(&self, name: EventHeader) -> Option<&str> {
92        HeaderLookup::header(self, name)
93    }
94
95    /// Look up a header by name, in any casing the switch might have spelled
96    /// it.
97    ///
98    /// Use [`header()`](Self::header) with an [`EventHeader`] variant for known
99    /// headers. This method is for headers not (yet) covered by the enum,
100    /// such as custom `X-` headers or FreeSWITCH headers added after this
101    /// library was published.
102    pub fn header_str(&self, name: &str) -> Option<&str> {
103        self.headers
104            .header_str(name)
105    }
106
107    /// Look up a channel variable by its bare name.
108    ///
109    /// Equivalent to [`variable()`](Self::variable) but matches the
110    /// [`HeaderLookup`] trait signature.
111    pub fn variable_str(&self, name: &str) -> Option<&str> {
112        self.header_str(&variable_key(name))
113    }
114
115    /// All headers as a map, keyed canonically.
116    pub fn headers(&self) -> &IndexMap<String, String> {
117        self.headers
118            .as_map()
119    }
120
121    /// Set or overwrite a header, normalizing the key.
122    pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
123        self.headers
124            .insert(name, value);
125    }
126
127    /// Remove a header, returning its value if it existed.
128    ///
129    /// Accepts both canonical and original (non-normalized) key names.
130    pub fn remove_header(&mut self, name: impl AsRef<str>) -> Option<String> {
131        self.headers
132            .remove(name.as_ref())
133    }
134
135    /// Event body (the content after the blank line in plain-text events).
136    pub fn body(&self) -> Option<&str> {
137        self.body
138            .as_deref()
139    }
140
141    /// Set the event body.
142    pub fn set_body(&mut self, body: impl Into<String>) {
143        self.body = Some(body.into());
144    }
145
146    /// Exact wire bytes of the event body when it was not valid UTF-8.
147    ///
148    /// `Some` is the lossy signal: [`body()`](Self::body) then holds the
149    /// U+FFFD-substituted string and these are the original payload bytes
150    /// (e.g. a Latin-1 SMS body), so the app can re-decode or audit them.
151    /// For plain and log events these are the inner body bytes. For JSON/XML
152    /// events, wire bytes cannot be mapped back to the decoded body, so this
153    /// carries the whole event envelope body (the serialized JSON/XML
154    /// document as sent on the wire) — the signal is still observable.
155    /// `None` in the normal case.
156    pub fn raw_body(&self) -> Option<&[u8]> {
157        self.raw_body
158            .as_deref()
159    }
160
161    /// Set the raw body bytes.
162    ///
163    /// Used internally by the ESL parser; consumers don't call this directly.
164    #[doc(hidden)]
165    pub fn set_raw_body(&mut self, bytes: Vec<u8>) {
166        self.raw_body = Some(bytes);
167    }
168
169    /// Headers whose percent-decoded value contained invalid UTF-8 and was
170    /// decoded lossily (U+FFFD substituted).
171    ///
172    /// Each entry carries the on-wire `raw_value()` (the percent-encoded source
173    /// text) so the app can re-decode it (e.g. as Latin-1) or audit it instead
174    /// of being stuck with the U+FFFD-substituted string in `headers`. Empty in
175    /// the normal case.
176    pub fn lossy_values(&self) -> &LossyValues {
177        &self.lossy_values
178    }
179
180    /// Set the lossy values.
181    ///
182    /// Used internally by the ESL parser; consumers don't call this directly.
183    #[doc(hidden)]
184    pub fn set_lossy_values(&mut self, v: LossyValues) {
185        self.lossy_values = v;
186    }
187
188    /// Sets the `priority` header carried on the event.
189    ///
190    /// FreeSWITCH stores this as metadata but does **not** use it for dispatch
191    /// ordering -- all events are delivered FIFO regardless of priority.
192    pub fn set_priority(&mut self, priority: EslEventPriority) {
193        self.set_header(EventHeader::Priority.as_str(), priority.to_string());
194    }
195
196    /// Append a value to a multi-value header (PUSH semantics).
197    ///
198    /// If the header doesn't exist, sets it as a plain value.
199    /// If it exists as a plain value, converts to `ARRAY::old|:new`.
200    /// If it already has an `ARRAY::` prefix, appends the new value.
201    ///
202    /// Returns [`EslArrayError::TooManyItems`] if the existing header already
203    /// contains [`MAX_ARRAY_ITEMS`](crate::MAX_ARRAY_ITEMS) items.
204    ///
205    /// ```
206    /// # use freeswitch_types::EslEvent;
207    /// let mut event = EslEvent::new();
208    /// event.push_header("X-Test", "first").unwrap();
209    /// event.push_header("X-Test", "second").unwrap();
210    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
211    /// ```
212    pub fn push_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
213        self.stack_header(name, value, EslArray::push)
214    }
215
216    /// Prepend a value to a multi-value header (UNSHIFT semantics).
217    ///
218    /// Same conversion rules as [`push_header()`](Self::push_header), but
219    /// inserts at the front.
220    ///
221    /// ```
222    /// # use freeswitch_types::EslEvent;
223    /// let mut event = EslEvent::new();
224    /// event.set_header("X-Test", "ARRAY::b|:c");
225    /// event.unshift_header("X-Test", "a").unwrap();
226    /// assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
227    /// ```
228    pub fn unshift_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
229        self.stack_header(name, value, EslArray::unshift)
230    }
231
232    fn stack_header(
233        &mut self,
234        name: &str,
235        value: &str,
236        op: fn(&mut EslArray, String),
237    ) -> Result<(), EslArrayError> {
238        match self
239            .header_str(name)
240            .map(str::to_string)
241        {
242            None => {
243                self.set_header(name, value);
244            }
245            Some(existing) => {
246                let arr = match EslArray::parse(&existing) {
247                    Ok(arr) => arr,
248                    Err(EslArrayError::MissingPrefix) => EslArray::new(vec![existing]),
249                    Err(e) => return Err(e),
250                };
251                if arr.len() >= crate::variables::MAX_ARRAY_ITEMS {
252                    return Err(EslArrayError::TooManyItems {
253                        count: arr.len(),
254                        max: crate::variables::MAX_ARRAY_ITEMS,
255                    });
256                }
257                let mut arr = arr;
258                op(&mut arr, value.into());
259                self.set_header(name, arr.to_string());
260            }
261        }
262        Ok(())
263    }
264
265    /// Check whether this event matches the given type.
266    pub fn is_event_type(&self, event_type: EslEventType) -> bool {
267        self.event_type() == Some(event_type)
268    }
269
270    /// Serialize to ESL plain text wire format with percent-encoded header values.
271    ///
272    /// This is the inverse of `EslParser::parse_plain_event()`. The output can
273    /// be fed back through the parser to reconstruct an equivalent `EslEvent`
274    /// (round-trip).
275    ///
276    /// Headers are emitted in insertion order (which matches wire order when the
277    /// event was parsed from the network). `Content-Length` from stored headers
278    /// is skipped and recomputed from the body if present.
279    pub fn to_plain_format(&self) -> String {
280        use fmt::Write;
281        let mut result = String::new();
282
283        for (key, value) in self.headers() {
284            if key == "Content-Length" {
285                continue;
286            }
287            writeln!(
288                result,
289                "{}: {}",
290                key,
291                percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
292            )
293            .expect("writing to String is infallible");
294        }
295
296        if let Some(body) = &self.body {
297            writeln!(result, "Content-Length: {}", body.len())
298                .expect("writing to String is infallible");
299            result.push('\n');
300            result.push_str(body);
301        } else {
302            result.push('\n');
303        }
304
305        result
306    }
307}
308
309impl Default for EslEvent {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315impl HeaderLookup for EslEvent {
316    fn header_str(&self, name: &str) -> Option<&str> {
317        EslEvent::header_str(self, name)
318    }
319
320    fn variable_str(&self, name: &str) -> Option<&str> {
321        EslEvent::variable_str(self, name)
322    }
323}
324
325impl sip_header::SipHeaderLookup for EslEvent {
326    fn sip_header_str(&self, name: &str) -> Option<&str> {
327        EslEvent::header_str(self, name)
328    }
329
330    crate::esl_sip_header_overrides!();
331}
332
333impl PartialEq for EslEvent {
334    fn eq(&self, other: &Self) -> bool {
335        self.headers == other.headers && self.body == other.body
336    }
337}
338
339impl std::hash::Hash for EslEvent {
340    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
341        for (k, v) in self.headers() {
342            k.hash(state);
343            v.hash(state);
344        }
345        self.body
346            .hash(state);
347    }
348}
349
350#[cfg(feature = "serde")]
351impl<'de> serde::Deserialize<'de> for EslEvent {
352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353    where
354        D: serde::Deserializer<'de>,
355    {
356        #[derive(serde::Deserialize)]
357        struct Raw {
358            headers: EslHeaders,
359            body: Option<String>,
360            #[serde(default)]
361            raw_body: Option<Vec<u8>>,
362            #[serde(default)]
363            lossy_values: LossyValues,
364        }
365        let raw = Raw::deserialize(deserializer)?;
366        Ok(EslEvent {
367            headers: raw.headers,
368            body: raw.body,
369            raw_body: raw.raw_body,
370            lossy_values: raw.lossy_values,
371        })
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn headers_preserve_insertion_order() {
381        let mut event = EslEvent::new();
382        event.set_header("Zebra", "last");
383        event.set_header("Alpha", "first");
384        event.set_header("Middle", "mid");
385        let keys: Vec<&str> = event
386            .headers()
387            .keys()
388            .map(|s| s.as_str())
389            .collect();
390        assert_eq!(keys, vec!["Zebra", "Alpha", "Middle"]);
391    }
392
393    #[test]
394    fn test_remove_header() {
395        let mut event = EslEvent::new();
396        event.set_header("Foo", "bar");
397        event.set_header("Baz", "qux");
398
399        let removed = event.remove_header("Foo");
400        assert_eq!(removed, Some("bar".to_string()));
401        assert!(event
402            .header_str("Foo")
403            .is_none());
404        assert_eq!(event.header_str("Baz"), Some("qux"));
405
406        let removed_again = event.remove_header("Foo");
407        assert_eq!(removed_again, None);
408    }
409
410    #[test]
411    fn test_to_plain_format_basic() {
412        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
413        event.set_header("Event-Name", "HEARTBEAT");
414        event.set_header("Core-UUID", "abc-123");
415
416        let plain = event.to_plain_format();
417
418        assert!(plain.starts_with("Event-Name: "));
419        assert!(plain.contains("Core-UUID: "));
420        assert!(plain.ends_with("\n\n"));
421    }
422
423    #[test]
424    fn test_to_plain_format_percent_encoding() {
425        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
426        event.set_header("Event-Name", "HEARTBEAT");
427        event.set_header("Up-Time", "0 years, 0 days");
428
429        let plain = event.to_plain_format();
430
431        assert!(!plain.contains("0 years, 0 days"));
432        assert!(plain.contains("Up-Time: "));
433        assert!(plain.contains("%20"));
434    }
435
436    #[test]
437    fn test_to_plain_format_with_body() {
438        let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
439        event.set_header("Event-Name", "BACKGROUND_JOB");
440        event.set_header("Job-UUID", "def-456");
441        event.set_body("+OK result\n".to_string());
442
443        let plain = event.to_plain_format();
444
445        assert!(plain.contains("Content-Length: 11\n"));
446        assert!(plain.ends_with("\n\n+OK result\n"));
447    }
448
449    #[test]
450    fn test_to_plain_format_preserves_insertion_order() {
451        let mut event = EslEvent::with_type(EslEventType::Heartbeat);
452        event.set_header("Event-Name", "HEARTBEAT");
453        event.set_header("Core-UUID", "abc-123");
454        event.set_header("FreeSWITCH-Hostname", "fs01");
455        event.set_header("Up-Time", "0 years, 1 day");
456
457        let plain = event.to_plain_format();
458        let lines: Vec<&str> = plain
459            .lines()
460            .collect();
461        assert!(lines[0].starts_with("Event-Name: "));
462        assert!(lines[1].starts_with("Core-UUID: "));
463        assert!(lines[2].starts_with("FreeSWITCH-Hostname: "));
464        assert!(lines[3].starts_with("Up-Time: "));
465    }
466
467    #[test]
468    fn test_set_priority_normal() {
469        let mut event = EslEvent::new();
470        event.set_priority(EslEventPriority::Normal);
471        assert_eq!(
472            event
473                .priority()
474                .unwrap(),
475            Some(EslEventPriority::Normal)
476        );
477        assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
478    }
479
480    #[test]
481    fn test_set_priority_high() {
482        let mut event = EslEvent::new();
483        event.set_priority(EslEventPriority::High);
484        assert_eq!(
485            event
486                .priority()
487                .unwrap(),
488            Some(EslEventPriority::High)
489        );
490        assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
491    }
492
493    #[test]
494    fn test_push_header_new() {
495        let mut event = EslEvent::new();
496        event
497            .push_header("X-Test", "first")
498            .unwrap();
499        assert_eq!(event.header_str("X-Test"), Some("first"));
500    }
501
502    #[test]
503    fn test_push_header_existing_plain() {
504        let mut event = EslEvent::new();
505        event.set_header("X-Test", "first");
506        event
507            .push_header("X-Test", "second")
508            .unwrap();
509        assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
510    }
511
512    #[test]
513    fn test_push_header_existing_array() {
514        let mut event = EslEvent::new();
515        event.set_header("X-Test", "ARRAY::a|:b");
516        event
517            .push_header("X-Test", "c")
518            .unwrap();
519        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
520    }
521
522    #[test]
523    fn test_push_header_at_capacity() {
524        use crate::variables::MAX_ARRAY_ITEMS;
525        let mut event = EslEvent::new();
526        let items: Vec<&str> = (0..MAX_ARRAY_ITEMS)
527            .map(|_| "x")
528            .collect();
529        event.set_header("X-Test", format!("ARRAY::{}", items.join("|:")).as_str());
530        assert!(matches!(
531            event.push_header("X-Test", "overflow"),
532            Err(EslArrayError::TooManyItems { .. })
533        ));
534    }
535
536    #[test]
537    fn test_unshift_header_new() {
538        let mut event = EslEvent::new();
539        event
540            .unshift_header("X-Test", "only")
541            .unwrap();
542        assert_eq!(event.header_str("X-Test"), Some("only"));
543    }
544
545    #[test]
546    fn test_unshift_header_existing_array() {
547        let mut event = EslEvent::new();
548        event.set_header("X-Test", "ARRAY::b|:c");
549        event
550            .unshift_header("X-Test", "a")
551            .unwrap();
552        assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
553    }
554
555    #[test]
556    fn test_sendevent_with_priority_wire_format() {
557        let mut event = EslEvent::with_type(EslEventType::Custom);
558        event.set_header("Event-Name", "CUSTOM");
559        event.set_header("Event-Subclass", "test::priority");
560        event.set_priority(EslEventPriority::High);
561
562        let plain = event.to_plain_format();
563        assert!(plain.contains("priority: HIGH\n"));
564    }
565
566    #[test]
567    fn test_convenience_accessors() {
568        let mut event = EslEvent::new();
569        event.set_header("Channel-Name", "sofia/internal/1000@example.com");
570        event.set_header("Caller-Caller-ID-Number", "1000");
571        event.set_header("Caller-Caller-ID-Name", "Alice");
572        event.set_header("Hangup-Cause", "NORMAL_CLEARING");
573        event.set_header("Event-Subclass", "sofia::register");
574        event.set_header("variable_sip_from_display", "Bob");
575
576        assert_eq!(
577            event.channel_name(),
578            Some("sofia/internal/1000@example.com")
579        );
580        assert_eq!(event.caller_id_number(), Some("1000"));
581        assert_eq!(event.caller_id_name(), Some("Alice"));
582        assert_eq!(
583            event
584                .hangup_cause()
585                .unwrap(),
586            Some(crate::channel::HangupCause::NormalClearing)
587        );
588        assert_eq!(event.event_subclass(), Some("sofia::register"));
589        assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
590        assert_eq!(event.variable_str("nonexistent"), None);
591    }
592
593    // --- EslEvent accessor tests (via HeaderLookup trait) ---
594
595    #[test]
596    fn test_event_channel_state_accessor() {
597        use crate::channel::ChannelState;
598        let mut event = EslEvent::new();
599        event.set_header("Channel-State", "CS_EXECUTE");
600        assert_eq!(
601            event
602                .channel_state()
603                .unwrap(),
604            Some(ChannelState::CsExecute)
605        );
606    }
607
608    #[test]
609    fn test_event_channel_state_number_accessor() {
610        use crate::channel::ChannelState;
611        let mut event = EslEvent::new();
612        event.set_header("Channel-State-Number", "4");
613        assert_eq!(
614            event
615                .channel_state_number()
616                .unwrap(),
617            Some(ChannelState::CsExecute)
618        );
619    }
620
621    #[test]
622    fn test_event_call_state_accessor() {
623        use crate::channel::CallState;
624        let mut event = EslEvent::new();
625        event.set_header("Channel-Call-State", "ACTIVE");
626        assert_eq!(
627            event
628                .call_state()
629                .unwrap(),
630            Some(CallState::Active)
631        );
632    }
633
634    #[test]
635    fn test_event_answer_state_accessor() {
636        use crate::channel::AnswerState;
637        let mut event = EslEvent::new();
638        event.set_header("Answer-State", "answered");
639        assert_eq!(
640            event
641                .answer_state()
642                .unwrap(),
643            Some(AnswerState::Answered)
644        );
645    }
646
647    #[test]
648    fn test_event_call_direction_accessor() {
649        use crate::channel::CallDirection;
650        let mut event = EslEvent::new();
651        event.set_header("Call-Direction", "inbound");
652        assert_eq!(
653            event
654                .call_direction()
655                .unwrap(),
656            Some(CallDirection::Inbound)
657        );
658    }
659
660    #[test]
661    fn test_event_typed_accessors_missing_headers() {
662        let event = EslEvent::new();
663        assert_eq!(
664            event
665                .channel_state()
666                .unwrap(),
667            None
668        );
669        assert_eq!(
670            event
671                .channel_state_number()
672                .unwrap(),
673            None
674        );
675        assert_eq!(
676            event
677                .call_state()
678                .unwrap(),
679            None
680        );
681        assert_eq!(
682            event
683                .answer_state()
684                .unwrap(),
685            None
686        );
687        assert_eq!(
688            event
689                .call_direction()
690                .unwrap(),
691            None
692        );
693    }
694
695    // --- Repeating SIP header tests ---
696
697    #[test]
698    fn test_sip_p_asserted_identity_comma_separated() {
699        let mut event = EslEvent::new();
700        // RFC 3325: P-Asserted-Identity can carry two identities (one sip:, one tel:)
701        // FreeSWITCH stores the comma-separated value as a single channel variable
702        event.set_header(
703            "variable_sip_P-Asserted-Identity",
704            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
705        );
706
707        assert_eq!(
708            event.variable_str("sip_P-Asserted-Identity"),
709            Some("<sip:alice@atlanta.example.com>, <tel:+15551234567>")
710        );
711    }
712
713    #[test]
714    fn test_sip_p_asserted_identity_array_format() {
715        let mut event = EslEvent::new();
716        // When FreeSWITCH stores repeated SIP headers via ARRAY format
717        event
718            .push_header(
719                "variable_sip_P-Asserted-Identity",
720                "<sip:alice@atlanta.example.com>",
721            )
722            .unwrap();
723        event
724            .push_header("variable_sip_P-Asserted-Identity", "<tel:+15551234567>")
725            .unwrap();
726
727        let raw = event
728            .header_str("variable_sip_P-Asserted-Identity")
729            .unwrap();
730        assert_eq!(
731            raw,
732            "ARRAY::<sip:alice@atlanta.example.com>|:<tel:+15551234567>"
733        );
734
735        let arr = crate::variables::EslArray::parse(raw).unwrap();
736        assert_eq!(arr.len(), 2);
737        assert_eq!(arr.items()[0], "<sip:alice@atlanta.example.com>");
738        assert_eq!(arr.items()[1], "<tel:+15551234567>");
739    }
740
741    #[test]
742    fn test_sip_header_with_colons_in_uri() {
743        let mut event = EslEvent::new();
744        // SIP URIs contain colons (sip:, sips:) which must not confuse ARRAY parsing
745        event
746            .push_header(
747                "variable_sip_h_Diversion",
748                "<sip:+15551234567@gw.example.com;reason=unconditional>",
749            )
750            .unwrap();
751        event
752            .push_header(
753                "variable_sip_h_Diversion",
754                "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>",
755            )
756            .unwrap();
757
758        let raw = event
759            .header_str("variable_sip_h_Diversion")
760            .unwrap();
761        let arr = crate::variables::EslArray::parse(raw).unwrap();
762        assert_eq!(arr.len(), 2);
763        assert_eq!(
764            arr.items()[0],
765            "<sip:+15551234567@gw.example.com;reason=unconditional>"
766        );
767        assert_eq!(
768            arr.items()[1],
769            "<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>"
770        );
771    }
772
773    #[test]
774    fn test_sip_p_asserted_identity_plain_format_round_trip() {
775        let mut event = EslEvent::with_type(EslEventType::ChannelCreate);
776        event.set_header("Event-Name", "CHANNEL_CREATE");
777        event.set_header(
778            "variable_sip_P-Asserted-Identity",
779            "<sip:alice@atlanta.example.com>, <tel:+15551234567>",
780        );
781
782        let plain = event.to_plain_format();
783        // The comma-separated value should be percent-encoded on the wire
784        assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
785        // Angle brackets and comma should be encoded
786        assert!(!plain.contains("<sip:alice"));
787    }
788
789    // --- Header key normalization on EslEvent ---
790
791    #[test]
792    fn set_header_normalizes_known_enum_variant() {
793        let mut event = EslEvent::new();
794        event.set_header("unique-id", "abc-123");
795        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
796    }
797
798    #[test]
799    fn set_header_keeps_both_profile_name_spellings() {
800        let mut event = EslEvent::new();
801        event.set_header("profile-name", "internal");
802        event.set_header("profile_name", "external");
803        assert_eq!(event.header(EventHeader::ProfileName), Some("internal"));
804        assert_eq!(
805            event.header(EventHeader::ProfileNameSnake),
806            Some("external")
807        );
808        assert_eq!(event.profile_name(), Some("internal"));
809    }
810
811    #[test]
812    fn set_header_normalizes_codec_header() {
813        let mut event = EslEvent::new();
814        event.set_header("channel-read-codec-bit-rate", "128000");
815        assert_eq!(
816            event.header(EventHeader::ChannelReadCodecBitRate),
817            Some("128000")
818        );
819    }
820
821    #[test]
822    fn header_str_finds_by_original_key() {
823        let mut event = EslEvent::new();
824        event.set_header("unique-id", "abc-123");
825        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
826        assert_eq!(event.header_str("Unique-ID"), Some("abc-123"));
827    }
828
829    #[test]
830    fn header_str_finds_unknown_dash_header_by_original() {
831        let mut event = EslEvent::new();
832        event.set_header("x-custom-header", "val");
833        assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
834        assert_eq!(event.header_str("x-custom-header"), Some("val"));
835    }
836
837    #[test]
838    fn set_header_underscore_passthrough_preserves_sip_h() {
839        let mut event = EslEvent::new();
840        event.set_header("variable_sip_h_X-My-CUSTOM-Header", "val");
841        assert_eq!(
842            event.header_str("variable_sip_h_X-My-CUSTOM-Header"),
843            Some("val")
844        );
845    }
846
847    #[test]
848    fn set_header_different_casing_overwrites() {
849        let mut event = EslEvent::new();
850        event.set_header("Unique-ID", "first");
851        event.set_header("unique-id", "second");
852        // Both normalize to "Unique-ID", second overwrites first
853        assert_eq!(event.header(EventHeader::UniqueId), Some("second"));
854    }
855
856    #[test]
857    fn remove_header_by_original_key() {
858        let mut event = EslEvent::new();
859        event.set_header("unique-id", "abc-123");
860        let removed = event.remove_header("unique-id");
861        assert_eq!(removed, Some("abc-123".to_string()));
862        assert_eq!(event.header(EventHeader::UniqueId), None);
863    }
864
865    #[test]
866    fn remove_header_by_canonical_key() {
867        let mut event = EslEvent::new();
868        event.set_header("unique-id", "abc-123");
869        let removed = event.remove_header("Unique-ID");
870        assert_eq!(removed, Some("abc-123".to_string()));
871        assert_eq!(event.header_str("unique-id"), None);
872    }
873
874    #[test]
875    fn serde_round_trip_preserves_canonical_lookups() {
876        let mut event = EslEvent::new();
877        event.set_header("unique-id", "abc-123");
878        event.set_header("channel-read-codec-bit-rate", "128000");
879        let json = serde_json::to_string(&event).unwrap();
880        let deserialized: EslEvent = serde_json::from_str(&json).unwrap();
881        assert_eq!(deserialized.header(EventHeader::UniqueId), Some("abc-123"));
882        assert_eq!(
883            deserialized.header(EventHeader::ChannelReadCodecBitRate),
884            Some("128000")
885        );
886    }
887
888    #[test]
889    fn serde_deserialize_normalizes_external_json() {
890        let json = r#"{"event_type":null,"headers":{"unique-id":"abc-123","channel-read-codec-bit-rate":"128000"},"body":null}"#;
891        let event: EslEvent = serde_json::from_str(json).unwrap();
892        assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
893        assert_eq!(
894            event.header(EventHeader::ChannelReadCodecBitRate),
895            Some("128000")
896        );
897        assert_eq!(event.header_str("unique-id"), Some("abc-123"));
898    }
899
900    #[test]
901    fn both_spellings_resolve_after_serde_roundtrip() {
902        // A CODEC event carries `Channel-Write-Codec-Name` alongside
903        // `channel-write-codec-bit-rate`, and JSON-format events arrive that
904        // way too, so deserialization is a wire entry point like any other.
905        let external_json = r#"{
906            "event_type": null,
907            "headers": {
908                "Channel-Write-Codec-Name": "opus",
909                "channel-write-codec-bit-rate": "64000",
910                "Custom-X-Header": "preserved"
911            },
912            "body": null
913        }"#;
914        let parsed: EslEvent = serde_json::from_str(external_json).unwrap();
915
916        assert_eq!(
917            parsed.header(EventHeader::ChannelWriteCodecName),
918            Some("opus")
919        );
920        assert_eq!(
921            parsed.header(EventHeader::ChannelWriteCodecBitRate),
922            Some("64000")
923        );
924
925        assert_eq!(
926            parsed.header_str("channel-write-codec-bit-rate"),
927            Some("64000")
928        );
929        assert_eq!(
930            parsed.header_str("Channel-Write-Codec-Bit-Rate"),
931            Some("64000")
932        );
933        assert_eq!(parsed.header_str("Custom-X-Header"), Some("preserved"));
934
935        let json = serde_json::to_string(&parsed).unwrap();
936        let re_parsed: EslEvent = serde_json::from_str(&json).unwrap();
937        assert_eq!(
938            re_parsed.header(EventHeader::ChannelWriteCodecBitRate),
939            Some("64000")
940        );
941    }
942
943    #[test]
944    fn test_event_typed_accessors_invalid_values() {
945        let mut event = EslEvent::new();
946        event.set_header("Channel-State", "BOGUS");
947        event.set_header("Channel-State-Number", "999");
948        event.set_header("Channel-Call-State", "BOGUS");
949        event.set_header("Answer-State", "bogus");
950        event.set_header("Call-Direction", "bogus");
951        assert!(event
952            .channel_state()
953            .is_err());
954        assert!(event
955            .channel_state_number()
956            .is_err());
957        assert!(event
958            .call_state()
959            .is_err());
960        assert!(event
961            .answer_state()
962            .is_err());
963        assert!(event
964            .call_direction()
965            .is_err());
966    }
967
968    // --- SipHeaderLookup over FreeSWITCH's ARRAY encoding ---
969
970    #[test]
971    fn esl_event_call_info_array_encoding() {
972        use sip_header::SipHeaderLookup;
973
974        let mut event = EslEvent::new();
975        event.set_header(
976            "Call-Info".to_string(),
977            "ARRAY::<urn:emergency:uid:callid:abc>;purpose=emergency-CallId\
978             |:<urn:emergency:uid:incidentid:def>;purpose=emergency-IncidentId"
979                .to_string(),
980        );
981        let ci = event
982            .call_info()
983            .expect("should parse")
984            .expect("should be present");
985        assert_eq!(
986            ci.entries()
987                .len(),
988            2,
989            "ARRAY:: entries should expand"
990        );
991    }
992
993    #[test]
994    fn esl_event_call_info_plain_value_unchanged() {
995        use sip_header::SipHeaderLookup;
996
997        let mut event = EslEvent::new();
998        event.set_header(
999            "Call-Info".to_string(),
1000            "<sip:pbx.example.com>;purpose=icon".to_string(),
1001        );
1002        let ci = event
1003            .call_info()
1004            .expect("plain value should parse")
1005            .expect("should be present");
1006        assert_eq!(
1007            ci.entries()
1008                .len(),
1009            1
1010        );
1011    }
1012
1013    #[test]
1014    fn esl_event_history_info_array_encoding() {
1015        use sip_header::SipHeaderLookup;
1016
1017        let mut event = EslEvent::new();
1018        event.set_header(
1019            "History-Info".to_string(),
1020            "ARRAY::<sip:user@pbx.example.com>;index=1\
1021             |:<sip:forward@pbx.example.com?Reason=unconditional>;index=1.1"
1022                .to_string(),
1023        );
1024        let hi = event
1025            .history_info()
1026            .expect("should parse")
1027            .expect("should be present");
1028        assert_eq!(
1029            hi.entries()
1030                .len(),
1031            2,
1032            "ARRAY:: entries should expand"
1033        );
1034    }
1035}