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