1mod 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 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22 pub enum EslEventPriority {
23 Normal => "NORMAL",
25 Low => "LOW",
27 High => "HIGH",
29 }
30 error ParsePriorityError("priority");
31 tests: esl_event_priority_wire_tests;
32}
33
34#[derive(Debug, Clone, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37pub struct EslEvent {
38 headers: IndexMap<String, String>,
39 #[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 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 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 pub fn event_type(&self) -> Option<EslEventType> {
86 self.header(EventHeader::EventName)
87 .and_then(EslEventType::parse_event_type)
88 }
89
90 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 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 pub fn variable_str(&self, name: &str) -> Option<&str> {
125 let key = format!("variable_{}", name);
126 self.header_str(&key)
127 }
128
129 pub fn headers(&self) -> &IndexMap<String, String> {
131 &self.headers
132 }
133
134 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 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 pub fn body(&self) -> Option<&str> {
170 self.body
171 .as_deref()
172 }
173
174 pub fn set_body(&mut self, body: impl Into<String>) {
176 self.body = Some(body.into());
177 }
178
179 pub fn lossy_values(&self) -> &LossyValues {
187 &self.lossy_values
188 }
189
190 #[doc(hidden)]
194 pub fn set_lossy_values(&mut self, v: LossyValues) {
195 self.lossy_values = v;
196 }
197
198 pub fn set_priority(&mut self, priority: EslEventPriority) {
203 self.set_header(EventHeader::Priority.as_str(), priority.to_string());
204 }
205
206 pub fn push_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
223 self.stack_header(name, value, EslArray::push)
224 }
225
226 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 pub fn is_event_type(&self, event_type: EslEventType) -> bool {
277 self.event_type() == Some(event_type)
278 }
279
280 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 #[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 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 #[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 #[test]
818 fn test_sip_p_asserted_identity_comma_separated() {
819 let mut event = EslEvent::new();
820 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 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 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 assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
905 assert!(!plain.contains("<sip:alice"));
907 }
908
909 #[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 assert_eq!(event.header_str("unique-id"), Some("abc-123"));
936 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 assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
946 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 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 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 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 assert_eq!(
1057 parsed.header_str("channel-write-codec-bit-rate"),
1058 Some("64000")
1059 );
1060 assert_eq!(
1062 parsed.header_str("Channel-Write-Codec-Bit-Rate"),
1063 Some("64000")
1064 );
1065
1066 assert_eq!(parsed.header_str("Custom-X-Header"), Some("preserved"));
1069
1070 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 #[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}