mod event_type;
mod format;
mod subscription;
pub use event_type::{EslEventType, ParseEventTypeError};
pub use format::{EventFormat, ParseEventFormatError};
pub use subscription::{EventSubscription, EventSubscriptionError};
use crate::headers::{case_alias_key, normalize_header_key, EventHeader};
use crate::lookup::HeaderLookup;
use crate::lossy_values::LossyValues;
use crate::variables::{EslArray, EslArrayError};
use indexmap::IndexMap;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use std::fmt;
wire_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EslEventPriority {
Normal => "NORMAL",
Low => "LOW",
High => "HIGH",
}
error ParsePriorityError("priority");
tests: esl_event_priority_wire_tests;
}
#[derive(Debug, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct EslEvent {
headers: IndexMap<String, String>,
#[cfg_attr(feature = "serde", serde(skip))]
original_keys: IndexMap<String, String>,
body: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
raw_body: Option<Vec<u8>>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "LossyValues::is_empty")
)]
lossy_values: LossyValues,
}
impl EslEvent {
pub fn new() -> Self {
Self {
headers: IndexMap::new(),
original_keys: IndexMap::new(),
body: None,
raw_body: None,
lossy_values: LossyValues::default(),
}
}
pub fn with_type(event_type: EslEventType) -> Self {
let mut event = Self::new();
event.set_header(EventHeader::EventName.as_str(), event_type.as_str());
event
}
pub fn event_type(&self) -> Option<EslEventType> {
self.header(EventHeader::EventName)
.and_then(EslEventType::parse_event_type)
}
pub fn header(&self, name: EventHeader) -> Option<&str> {
self.headers
.get(name.as_str())
.map(|s| s.as_str())
}
pub fn header_str(&self, name: &str) -> Option<&str> {
self.headers
.get(name)
.or_else(|| {
self.original_keys
.get(name)
.and_then(|normalized| {
self.headers
.get(normalized)
})
})
.map(|s| s.as_str())
}
pub fn variable_str(&self, name: &str) -> Option<&str> {
let key = format!("variable_{}", name);
self.header_str(&key)
}
pub fn headers(&self) -> &IndexMap<String, String> {
&self.headers
}
pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
let original = name.into();
let normalized = normalize_header_key(&original);
if case_alias_key(&original).is_some() && original != normalized {
self.original_keys
.insert(original, normalized.clone());
}
self.headers
.insert(normalized, value.into());
}
pub fn remove_header(&mut self, name: impl AsRef<str>) -> Option<String> {
let name = name.as_ref();
if let Some(value) = self
.headers
.shift_remove(name)
{
return Some(value);
}
if let Some(normalized) = self
.original_keys
.shift_remove(name)
{
return self
.headers
.shift_remove(&normalized);
}
None
}
pub fn body(&self) -> Option<&str> {
self.body
.as_deref()
}
pub fn set_body(&mut self, body: impl Into<String>) {
self.body = Some(body.into());
}
pub fn raw_body(&self) -> Option<&[u8]> {
self.raw_body
.as_deref()
}
#[doc(hidden)]
pub fn set_raw_body(&mut self, bytes: Vec<u8>) {
self.raw_body = Some(bytes);
}
pub fn lossy_values(&self) -> &LossyValues {
&self.lossy_values
}
#[doc(hidden)]
pub fn set_lossy_values(&mut self, v: LossyValues) {
self.lossy_values = v;
}
pub fn set_priority(&mut self, priority: EslEventPriority) {
self.set_header(EventHeader::Priority.as_str(), priority.to_string());
}
pub fn push_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
self.stack_header(name, value, EslArray::push)
}
pub fn unshift_header(&mut self, name: &str, value: &str) -> Result<(), EslArrayError> {
self.stack_header(name, value, EslArray::unshift)
}
fn stack_header(
&mut self,
name: &str,
value: &str,
op: fn(&mut EslArray, String),
) -> Result<(), EslArrayError> {
match self
.headers
.get(name)
{
None => {
self.set_header(name, value);
}
Some(existing) => {
let arr = match EslArray::parse(existing) {
Ok(arr) => arr,
Err(EslArrayError::MissingPrefix) => EslArray::new(vec![existing.clone()]),
Err(e) => return Err(e),
};
if arr.len() >= crate::variables::MAX_ARRAY_ITEMS {
return Err(EslArrayError::TooManyItems {
count: arr.len(),
max: crate::variables::MAX_ARRAY_ITEMS,
});
}
let mut arr = arr;
op(&mut arr, value.into());
self.set_header(name, arr.to_string());
}
}
Ok(())
}
pub fn is_event_type(&self, event_type: EslEventType) -> bool {
self.event_type() == Some(event_type)
}
pub fn to_plain_format(&self) -> String {
use fmt::Write;
let mut result = String::new();
for (key, value) in &self.headers {
if key == "Content-Length" {
continue;
}
writeln!(
result,
"{}: {}",
key,
percent_encode(value.as_bytes(), NON_ALPHANUMERIC)
)
.expect("writing to String is infallible");
}
if let Some(body) = &self.body {
writeln!(result, "Content-Length: {}", body.len())
.expect("writing to String is infallible");
result.push('\n');
result.push_str(body);
} else {
result.push('\n');
}
result
}
}
impl Default for EslEvent {
fn default() -> Self {
Self::new()
}
}
impl HeaderLookup for EslEvent {
fn header_str(&self, name: &str) -> Option<&str> {
EslEvent::header_str(self, name)
}
fn variable_str(&self, name: &str) -> Option<&str> {
let key = format!("variable_{}", name);
self.header_str(&key)
}
}
impl sip_header::SipHeaderLookup for EslEvent {
fn sip_header_str(&self, name: &str) -> Option<&str> {
EslEvent::header_str(self, name)
}
fn call_info(&self) -> Result<Option<sip_header::UriInfo>, sip_header::UriInfoError> {
match self.sip_header(sip_header::SipHeader::CallInfo) {
Some(s) => crate::variables::EslHeaders::parse_uri_info(s).map(Some),
None => Ok(None),
}
}
fn history_info(
&self,
) -> Result<Option<sip_header::HistoryInfo>, sip_header::HistoryInfoError> {
match self.sip_header(sip_header::SipHeader::HistoryInfo) {
Some(s) => crate::variables::EslHeaders::parse_history_info(s).map(Some),
None => Ok(None),
}
}
fn alert_info(&self) -> Result<Option<sip_header::UriInfo>, sip_header::UriInfoError> {
match self.sip_header(sip_header::SipHeader::AlertInfo) {
Some(s) => crate::variables::EslHeaders::parse_uri_info(s).map(Some),
None => Ok(None),
}
}
}
impl PartialEq for EslEvent {
fn eq(&self, other: &Self) -> bool {
self.headers == other.headers && self.body == other.body
}
}
impl std::hash::Hash for EslEvent {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for (k, v) in &self.headers {
k.hash(state);
v.hash(state);
}
self.body
.hash(state);
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for EslEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct Raw {
#[serde(default)]
#[allow(dead_code)]
event_type: Option<EslEventType>,
headers: IndexMap<String, String>,
body: Option<String>,
#[serde(default)]
raw_body: Option<Vec<u8>>,
#[serde(default)]
lossy_values: LossyValues,
}
let raw = Raw::deserialize(deserializer)?;
let mut event = EslEvent::new();
event.body = raw.body;
event.raw_body = raw.raw_body;
event.lossy_values = raw.lossy_values;
for (k, v) in raw.headers {
event.set_header(k, v);
}
Ok(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headers_preserve_insertion_order() {
let mut event = EslEvent::new();
event.set_header("Zebra", "last");
event.set_header("Alpha", "first");
event.set_header("Middle", "mid");
let keys: Vec<&str> = event
.headers()
.keys()
.map(|s| s.as_str())
.collect();
assert_eq!(keys, vec!["Zebra", "Alpha", "Middle"]);
}
#[test]
fn test_remove_header() {
let mut event = EslEvent::new();
event.set_header("Foo", "bar");
event.set_header("Baz", "qux");
let removed = event.remove_header("Foo");
assert_eq!(removed, Some("bar".to_string()));
assert!(event
.header_str("Foo")
.is_none());
assert_eq!(event.header_str("Baz"), Some("qux"));
let removed_again = event.remove_header("Foo");
assert_eq!(removed_again, None);
}
#[test]
fn test_to_plain_format_basic() {
let mut event = EslEvent::with_type(EslEventType::Heartbeat);
event.set_header("Event-Name", "HEARTBEAT");
event.set_header("Core-UUID", "abc-123");
let plain = event.to_plain_format();
assert!(plain.starts_with("Event-Name: "));
assert!(plain.contains("Core-UUID: "));
assert!(plain.ends_with("\n\n"));
}
#[test]
fn test_to_plain_format_percent_encoding() {
let mut event = EslEvent::with_type(EslEventType::Heartbeat);
event.set_header("Event-Name", "HEARTBEAT");
event.set_header("Up-Time", "0 years, 0 days");
let plain = event.to_plain_format();
assert!(!plain.contains("0 years, 0 days"));
assert!(plain.contains("Up-Time: "));
assert!(plain.contains("%20"));
}
#[test]
fn test_to_plain_format_with_body() {
let mut event = EslEvent::with_type(EslEventType::BackgroundJob);
event.set_header("Event-Name", "BACKGROUND_JOB");
event.set_header("Job-UUID", "def-456");
event.set_body("+OK result\n".to_string());
let plain = event.to_plain_format();
assert!(plain.contains("Content-Length: 11\n"));
assert!(plain.ends_with("\n\n+OK result\n"));
}
#[test]
fn test_to_plain_format_preserves_insertion_order() {
let mut event = EslEvent::with_type(EslEventType::Heartbeat);
event.set_header("Event-Name", "HEARTBEAT");
event.set_header("Core-UUID", "abc-123");
event.set_header("FreeSWITCH-Hostname", "fs01");
event.set_header("Up-Time", "0 years, 1 day");
let plain = event.to_plain_format();
let lines: Vec<&str> = plain
.lines()
.collect();
assert!(lines[0].starts_with("Event-Name: "));
assert!(lines[1].starts_with("Core-UUID: "));
assert!(lines[2].starts_with("FreeSWITCH-Hostname: "));
assert!(lines[3].starts_with("Up-Time: "));
}
#[test]
fn test_to_plain_format_round_trip() {
let mut original = EslEvent::with_type(EslEventType::ChannelCreate);
original.set_header("Event-Name", "CHANNEL_CREATE");
original.set_header("Core-UUID", "abc-123");
original.set_header("Channel-Name", "sofia/internal/1000@example.com");
original.set_header("Caller-Caller-ID-Name", "Jérôme Poulin");
original.set_body("some body content");
let plain = original.to_plain_format();
let (header_section, inner_body) = if let Some(pos) = plain.find("\n\n") {
(&plain[..pos], Some(&plain[pos + 2..]))
} else {
(plain.as_str(), None)
};
let mut parsed = EslEvent::new();
for line in header_section.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(colon_pos) = line.find(':') {
let key = line[..colon_pos].trim();
if key == "Content-Length" {
continue;
}
let raw_value = line[colon_pos + 1..].trim();
let value = percent_encoding::percent_decode_str(raw_value)
.decode_utf8()
.unwrap()
.into_owned();
parsed.set_header(key, value);
}
}
if let Some(ib) = inner_body {
if !ib.is_empty() {
parsed.set_body(ib);
}
}
assert_eq!(original.headers(), parsed.headers());
assert_eq!(original.body(), parsed.body());
}
#[test]
fn test_set_priority_normal() {
let mut event = EslEvent::new();
event.set_priority(EslEventPriority::Normal);
assert_eq!(
event
.priority()
.unwrap(),
Some(EslEventPriority::Normal)
);
assert_eq!(event.header(EventHeader::Priority), Some("NORMAL"));
}
#[test]
fn test_set_priority_high() {
let mut event = EslEvent::new();
event.set_priority(EslEventPriority::High);
assert_eq!(
event
.priority()
.unwrap(),
Some(EslEventPriority::High)
);
assert_eq!(event.header(EventHeader::Priority), Some("HIGH"));
}
#[test]
fn test_priority_display() {
assert_eq!(EslEventPriority::Normal.to_string(), "NORMAL");
assert_eq!(EslEventPriority::Low.to_string(), "LOW");
assert_eq!(EslEventPriority::High.to_string(), "HIGH");
}
#[test]
fn test_priority_from_str() {
assert_eq!(
"NORMAL".parse::<EslEventPriority>(),
Ok(EslEventPriority::Normal)
);
assert_eq!("LOW".parse::<EslEventPriority>(), Ok(EslEventPriority::Low));
assert_eq!(
"HIGH".parse::<EslEventPriority>(),
Ok(EslEventPriority::High)
);
assert!("INVALID"
.parse::<EslEventPriority>()
.is_err());
}
#[test]
fn test_priority_from_str_rejects_wrong_case() {
assert!("normal"
.parse::<EslEventPriority>()
.is_err());
assert!("Low"
.parse::<EslEventPriority>()
.is_err());
assert!("hIgH"
.parse::<EslEventPriority>()
.is_err());
}
#[test]
fn test_push_header_new() {
let mut event = EslEvent::new();
event
.push_header("X-Test", "first")
.unwrap();
assert_eq!(event.header_str("X-Test"), Some("first"));
}
#[test]
fn test_push_header_existing_plain() {
let mut event = EslEvent::new();
event.set_header("X-Test", "first");
event
.push_header("X-Test", "second")
.unwrap();
assert_eq!(event.header_str("X-Test"), Some("ARRAY::first|:second"));
}
#[test]
fn test_push_header_existing_array() {
let mut event = EslEvent::new();
event.set_header("X-Test", "ARRAY::a|:b");
event
.push_header("X-Test", "c")
.unwrap();
assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
}
#[test]
fn test_push_header_at_capacity() {
use crate::variables::MAX_ARRAY_ITEMS;
let mut event = EslEvent::new();
let items: Vec<&str> = (0..MAX_ARRAY_ITEMS)
.map(|_| "x")
.collect();
event.set_header("X-Test", format!("ARRAY::{}", items.join("|:")).as_str());
assert!(matches!(
event.push_header("X-Test", "overflow"),
Err(EslArrayError::TooManyItems { .. })
));
}
#[test]
fn test_unshift_header_new() {
let mut event = EslEvent::new();
event
.unshift_header("X-Test", "only")
.unwrap();
assert_eq!(event.header_str("X-Test"), Some("only"));
}
#[test]
fn test_unshift_header_existing_array() {
let mut event = EslEvent::new();
event.set_header("X-Test", "ARRAY::b|:c");
event
.unshift_header("X-Test", "a")
.unwrap();
assert_eq!(event.header_str("X-Test"), Some("ARRAY::a|:b|:c"));
}
#[test]
fn test_sendevent_with_priority_wire_format() {
let mut event = EslEvent::with_type(EslEventType::Custom);
event.set_header("Event-Name", "CUSTOM");
event.set_header("Event-Subclass", "test::priority");
event.set_priority(EslEventPriority::High);
let plain = event.to_plain_format();
assert!(plain.contains("priority: HIGH\n"));
}
#[test]
fn test_convenience_accessors() {
let mut event = EslEvent::new();
event.set_header("Channel-Name", "sofia/internal/1000@example.com");
event.set_header("Caller-Caller-ID-Number", "1000");
event.set_header("Caller-Caller-ID-Name", "Alice");
event.set_header("Hangup-Cause", "NORMAL_CLEARING");
event.set_header("Event-Subclass", "sofia::register");
event.set_header("variable_sip_from_display", "Bob");
assert_eq!(
event.channel_name(),
Some("sofia/internal/1000@example.com")
);
assert_eq!(event.caller_id_number(), Some("1000"));
assert_eq!(event.caller_id_name(), Some("Alice"));
assert_eq!(
event
.hangup_cause()
.unwrap(),
Some(crate::channel::HangupCause::NormalClearing)
);
assert_eq!(event.event_subclass(), Some("sofia::register"));
assert_eq!(event.variable_str("sip_from_display"), Some("Bob"));
assert_eq!(event.variable_str("nonexistent"), None);
}
#[test]
fn test_event_channel_state_accessor() {
use crate::channel::ChannelState;
let mut event = EslEvent::new();
event.set_header("Channel-State", "CS_EXECUTE");
assert_eq!(
event
.channel_state()
.unwrap(),
Some(ChannelState::CsExecute)
);
}
#[test]
fn test_event_channel_state_number_accessor() {
use crate::channel::ChannelState;
let mut event = EslEvent::new();
event.set_header("Channel-State-Number", "4");
assert_eq!(
event
.channel_state_number()
.unwrap(),
Some(ChannelState::CsExecute)
);
}
#[test]
fn test_event_call_state_accessor() {
use crate::channel::CallState;
let mut event = EslEvent::new();
event.set_header("Channel-Call-State", "ACTIVE");
assert_eq!(
event
.call_state()
.unwrap(),
Some(CallState::Active)
);
}
#[test]
fn test_event_answer_state_accessor() {
use crate::channel::AnswerState;
let mut event = EslEvent::new();
event.set_header("Answer-State", "answered");
assert_eq!(
event
.answer_state()
.unwrap(),
Some(AnswerState::Answered)
);
}
#[test]
fn test_event_call_direction_accessor() {
use crate::channel::CallDirection;
let mut event = EslEvent::new();
event.set_header("Call-Direction", "inbound");
assert_eq!(
event
.call_direction()
.unwrap(),
Some(CallDirection::Inbound)
);
}
#[test]
fn test_event_typed_accessors_missing_headers() {
let event = EslEvent::new();
assert_eq!(
event
.channel_state()
.unwrap(),
None
);
assert_eq!(
event
.channel_state_number()
.unwrap(),
None
);
assert_eq!(
event
.call_state()
.unwrap(),
None
);
assert_eq!(
event
.answer_state()
.unwrap(),
None
);
assert_eq!(
event
.call_direction()
.unwrap(),
None
);
}
#[test]
fn test_sip_p_asserted_identity_comma_separated() {
let mut event = EslEvent::new();
event.set_header(
"variable_sip_P-Asserted-Identity",
"<sip:alice@atlanta.example.com>, <tel:+15551234567>",
);
assert_eq!(
event.variable_str("sip_P-Asserted-Identity"),
Some("<sip:alice@atlanta.example.com>, <tel:+15551234567>")
);
}
#[test]
fn test_sip_p_asserted_identity_array_format() {
let mut event = EslEvent::new();
event
.push_header(
"variable_sip_P-Asserted-Identity",
"<sip:alice@atlanta.example.com>",
)
.unwrap();
event
.push_header("variable_sip_P-Asserted-Identity", "<tel:+15551234567>")
.unwrap();
let raw = event
.header_str("variable_sip_P-Asserted-Identity")
.unwrap();
assert_eq!(
raw,
"ARRAY::<sip:alice@atlanta.example.com>|:<tel:+15551234567>"
);
let arr = crate::variables::EslArray::parse(raw).unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr.items()[0], "<sip:alice@atlanta.example.com>");
assert_eq!(arr.items()[1], "<tel:+15551234567>");
}
#[test]
fn test_sip_header_with_colons_in_uri() {
let mut event = EslEvent::new();
event
.push_header(
"variable_sip_h_Diversion",
"<sip:+15551234567@gw.example.com;reason=unconditional>",
)
.unwrap();
event
.push_header(
"variable_sip_h_Diversion",
"<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>",
)
.unwrap();
let raw = event
.header_str("variable_sip_h_Diversion")
.unwrap();
let arr = crate::variables::EslArray::parse(raw).unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(
arr.items()[0],
"<sip:+15551234567@gw.example.com;reason=unconditional>"
);
assert_eq!(
arr.items()[1],
"<sips:+15559876543@secure.example.com;reason=no-answer;counter=3>"
);
}
#[test]
fn test_sip_p_asserted_identity_plain_format_round_trip() {
let mut event = EslEvent::with_type(EslEventType::ChannelCreate);
event.set_header("Event-Name", "CHANNEL_CREATE");
event.set_header(
"variable_sip_P-Asserted-Identity",
"<sip:alice@atlanta.example.com>, <tel:+15551234567>",
);
let plain = event.to_plain_format();
assert!(plain.contains("variable_sip_P-Asserted-Identity:"));
assert!(!plain.contains("<sip:alice"));
}
#[test]
fn set_header_normalizes_known_enum_variant() {
let mut event = EslEvent::new();
event.set_header("unique-id", "abc-123");
assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
}
#[test]
fn set_header_normalizes_codec_header() {
let mut event = EslEvent::new();
event.set_header("channel-read-codec-bit-rate", "128000");
assert_eq!(
event.header(EventHeader::ChannelReadCodecBitRate),
Some("128000")
);
}
#[test]
fn header_str_finds_by_original_key() {
let mut event = EslEvent::new();
event.set_header("unique-id", "abc-123");
assert_eq!(event.header_str("unique-id"), Some("abc-123"));
assert_eq!(event.header_str("Unique-ID"), Some("abc-123"));
}
#[test]
fn header_str_finds_unknown_dash_header_by_original() {
let mut event = EslEvent::new();
event.set_header("x-custom-header", "val");
assert_eq!(event.header_str("X-Custom-Header"), Some("val"));
assert_eq!(event.header_str("x-custom-header"), Some("val"));
}
#[test]
fn set_header_underscore_passthrough_preserves_sip_h() {
let mut event = EslEvent::new();
event.set_header("variable_sip_h_X-My-CUSTOM-Header", "val");
assert_eq!(
event.header_str("variable_sip_h_X-My-CUSTOM-Header"),
Some("val")
);
}
#[test]
fn set_header_different_casing_overwrites() {
let mut event = EslEvent::new();
event.set_header("Unique-ID", "first");
event.set_header("unique-id", "second");
assert_eq!(event.header(EventHeader::UniqueId), Some("second"));
}
#[test]
fn remove_header_by_original_key() {
let mut event = EslEvent::new();
event.set_header("unique-id", "abc-123");
let removed = event.remove_header("unique-id");
assert_eq!(removed, Some("abc-123".to_string()));
assert_eq!(event.header(EventHeader::UniqueId), None);
}
#[test]
fn remove_header_by_canonical_key() {
let mut event = EslEvent::new();
event.set_header("unique-id", "abc-123");
let removed = event.remove_header("Unique-ID");
assert_eq!(removed, Some("abc-123".to_string()));
assert_eq!(event.header_str("unique-id"), None);
}
#[test]
fn serde_round_trip_preserves_canonical_lookups() {
let mut event = EslEvent::new();
event.set_header("unique-id", "abc-123");
event.set_header("channel-read-codec-bit-rate", "128000");
let json = serde_json::to_string(&event).unwrap();
let deserialized: EslEvent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.header(EventHeader::UniqueId), Some("abc-123"));
assert_eq!(
deserialized.header(EventHeader::ChannelReadCodecBitRate),
Some("128000")
);
}
#[test]
fn serde_deserialize_normalizes_external_json() {
let json = r#"{"event_type":null,"headers":{"unique-id":"abc-123","channel-read-codec-bit-rate":"128000"},"body":null}"#;
let event: EslEvent = serde_json::from_str(json).unwrap();
assert_eq!(event.header(EventHeader::UniqueId), Some("abc-123"));
assert_eq!(
event.header(EventHeader::ChannelReadCodecBitRate),
Some("128000")
);
assert_eq!(event.header_str("unique-id"), Some("abc-123"));
}
#[test]
fn original_keys_rebuilt_after_serde_roundtrip() {
let external_json = r#"{
"event_type": null,
"headers": {
"Channel-Write-Codec-Name": "opus",
"channel-write-codec-bit-rate": "64000",
"Custom-X-Header": "preserved"
},
"body": null
}"#;
let parsed: EslEvent = serde_json::from_str(external_json).unwrap();
assert_eq!(
parsed.header(EventHeader::ChannelWriteCodecName),
Some("opus")
);
assert_eq!(
parsed.header(EventHeader::ChannelWriteCodecBitRate),
Some("64000")
);
assert_eq!(
parsed.header_str("channel-write-codec-bit-rate"),
Some("64000")
);
assert_eq!(
parsed.header_str("Channel-Write-Codec-Bit-Rate"),
Some("64000")
);
assert_eq!(parsed.header_str("Custom-X-Header"), Some("preserved"));
let json = serde_json::to_string(&parsed).unwrap();
let re_parsed: EslEvent = serde_json::from_str(&json).unwrap();
assert_eq!(
re_parsed.header(EventHeader::ChannelWriteCodecBitRate),
Some("64000")
);
}
#[test]
fn test_event_typed_accessors_invalid_values() {
let mut event = EslEvent::new();
event.set_header("Channel-State", "BOGUS");
event.set_header("Channel-State-Number", "999");
event.set_header("Channel-Call-State", "BOGUS");
event.set_header("Answer-State", "bogus");
event.set_header("Call-Direction", "bogus");
assert!(event
.channel_state()
.is_err());
assert!(event
.channel_state_number()
.is_err());
assert!(event
.call_state()
.is_err());
assert!(event
.answer_state()
.is_err());
assert!(event
.call_direction()
.is_err());
}
#[test]
fn esl_event_call_info_array_encoding() {
use sip_header::SipHeaderLookup;
let mut event = EslEvent::new();
event.set_header(
"Call-Info".to_string(),
"ARRAY::<urn:emergency:uid:callid:abc>;purpose=emergency-CallId\
|:<urn:emergency:uid:incidentid:def>;purpose=emergency-IncidentId"
.to_string(),
);
let ci = event
.call_info()
.expect("should parse")
.expect("should be present");
assert_eq!(
ci.entries()
.len(),
2,
"ARRAY:: entries should expand"
);
}
#[test]
fn esl_event_call_info_plain_value_unchanged() {
use sip_header::SipHeaderLookup;
let mut event = EslEvent::new();
event.set_header(
"Call-Info".to_string(),
"<sip:pbx.example.com>;purpose=icon".to_string(),
);
let ci = event
.call_info()
.expect("plain value should parse")
.expect("should be present");
assert_eq!(
ci.entries()
.len(),
1
);
}
#[test]
fn esl_event_history_info_array_encoding() {
use sip_header::SipHeaderLookup;
let mut event = EslEvent::new();
event.set_header(
"History-Info".to_string(),
"ARRAY::<sip:user@pbx.example.com>;index=1\
|:<sip:forward@pbx.example.com?Reason=unconditional>;index=1.1"
.to_string(),
);
let hi = event
.history_info()
.expect("should parse")
.expect("should be present");
assert_eq!(
hi.entries()
.len(),
2,
"ARRAY:: entries should expand"
);
}
}