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