Skip to main content

freeswitch_types/event/
mod.rs

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