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