Skip to main content

freeswitch_types/
headers.rs

1//! Typed event header names for FreeSWITCH ESL events.
2
3sip_header::define_header_enum! {
4    tests_mod: event_header_generated_tests,
5    error_type: ParseEventHeaderError => "unknown event header",
6    /// Top-level header names that appear in FreeSWITCH ESL events.
7    ///
8    /// These are the headers on the parsed event itself (not protocol framing
9    /// headers like `Content-Type`). Use with [`EslEvent::header()`](crate::EslEvent::header) for
10    /// type-safe lookups.
11    pub enum EventHeader {
12        EventName => "Event-Name",
13        EventSubclass => "Event-Subclass",
14        UniqueId => "Unique-ID",
15        CallerUniqueId => "Caller-Unique-ID",
16        OtherLegUniqueId => "Other-Leg-Unique-ID",
17        ChannelCallUuid => "Channel-Call-UUID",
18        JobUuid => "Job-UUID",
19        ChannelName => "Channel-Name",
20        ChannelState => "Channel-State",
21        ChannelStateNumber => "Channel-State-Number",
22        ChannelCallState => "Channel-Call-State",
23        AnswerState => "Answer-State",
24        CallDirection => "Call-Direction",
25        HangupCause => "Hangup-Cause",
26        CallerCallerIdName => "Caller-Caller-ID-Name",
27        CallerCallerIdNumber => "Caller-Caller-ID-Number",
28        CallerOrigCallerIdName => "Caller-Orig-Caller-ID-Name",
29        CallerOrigCallerIdNumber => "Caller-Orig-Caller-ID-Number",
30        CallerCalleeIdName => "Caller-Callee-ID-Name",
31        CallerCalleeIdNumber => "Caller-Callee-ID-Number",
32        CallerDestinationNumber => "Caller-Destination-Number",
33        CallerContext => "Caller-Context",
34        CallerDirection => "Caller-Direction",
35        CallerNetworkAddr => "Caller-Network-Addr",
36        CoreUuid => "Core-UUID",
37        DtmfDigit => "DTMF-Digit",
38        Priority => "priority",
39        LogLevel => "Log-Level",
40        /// SIP NOTIFY body content (JSON payload from `NOTIFY_IN` events).
41        PlData => "pl_data",
42        /// SIP event package name from `NOTIFY_IN` events (e.g. `emergency-AbandonedCall`).
43        SipEvent => "event",
44        /// SIP content type from `NOTIFY_IN` events.
45        SipContentType => "sip_content_type",
46        /// Gateway that received the SIP NOTIFY.
47        GatewayName => "gateway_name",
48
49        // --- Codec (from switch_channel_event_set_data / switch_core_codec.c) ---
50        // Audio read
51        ChannelReadCodecName => "Channel-Read-Codec-Name",
52        ChannelReadCodecRate => "Channel-Read-Codec-Rate",
53        ChannelReadCodecBitRate => "Channel-Read-Codec-Bit-Rate",
54        /// Only present when actual_samples_per_second != samples_per_second.
55        ChannelReportedReadCodecRate => "Channel-Reported-Read-Codec-Rate",
56        // Audio write
57        ChannelWriteCodecName => "Channel-Write-Codec-Name",
58        ChannelWriteCodecRate => "Channel-Write-Codec-Rate",
59        ChannelWriteCodecBitRate => "Channel-Write-Codec-Bit-Rate",
60        /// Only present when actual_samples_per_second != samples_per_second.
61        ChannelReportedWriteCodecRate => "Channel-Reported-Write-Codec-Rate",
62        // Video read/write
63        ChannelVideoReadCodecName => "Channel-Video-Read-Codec-Name",
64        ChannelVideoReadCodecRate => "Channel-Video-Read-Codec-Rate",
65        ChannelVideoWriteCodecName => "Channel-Video-Write-Codec-Name",
66        ChannelVideoWriteCodecRate => "Channel-Video-Write-Codec-Rate",
67        /// Active session count from `HEARTBEAT` events.
68        SessionCount => "Session-Count",
69        FreeswitchHostname => "FreeSWITCH-Hostname",
70        FreeswitchSwitchname => "FreeSWITCH-Switchname",
71        FreeswitchIpv4 => "FreeSWITCH-IPv4",
72        FreeswitchIpv6 => "FreeSWITCH-IPv6",
73        FreeswitchVersion => "FreeSWITCH-Version",
74        FreeswitchDomain => "FreeSWITCH-Domain",
75        FreeswitchUser => "FreeSWITCH-User",
76
77        // --- Application (from switch_core_session.c) ---
78        Application => "Application",
79        ApplicationData => "Application-Data",
80        ApplicationResponse => "Application-Response",
81        ApplicationUuid => "Application-UUID",
82
83        // --- Event metadata (from switch_event_prep_for_delivery_detailed) ---
84        EventDateLocal => "Event-Date-Local",
85        EventDateGmt => "Event-Date-GMT",
86        EventDateTimestamp => "Event-Date-Timestamp",
87        EventCallingFile => "Event-Calling-File",
88        EventCallingFunction => "Event-Calling-Function",
89        EventCallingLineNumber => "Event-Calling-Line-Number",
90        EventSequence => "Event-Sequence",
91
92        // --- Channel basic data (from switch_channel_event_set_basic_data) ---
93        ChannelPresenceId => "Channel-Presence-ID",
94        ChannelPresenceData => "Channel-Presence-Data",
95        PresenceDataCols => "Presence-Data-Cols",
96        PresenceCallDirection => "Presence-Call-Direction",
97        ChannelHitDialplan => "Channel-HIT-Dialplan",
98        SessionExternalId => "Session-External-ID",
99        /// `originator` or `originatee` on bridged channel events.
100        OtherType => "Other-Type",
101
102        // --- Callstate change (from switch_channel_perform_set_callstate) ---
103        ChannelCallStateNumber => "Channel-Call-State-Number",
104        OriginalChannelCallState => "Original-Channel-Call-State",
105
106        // --- DTMF (from switch_channel_dequeue_dtmf) ---
107        DtmfDuration => "DTMF-Duration",
108        DtmfSource => "DTMF-Source",
109
110        // --- Caller profile (from switch_caller_profile_event_set_data, "Caller-" prefix) ---
111        CallerLogicalDirection => "Caller-Logical-Direction",
112        CallerUsername => "Caller-Username",
113        CallerDialplan => "Caller-Dialplan",
114        CallerAni => "Caller-ANI",
115        CallerAniii => "Caller-ANI-II",
116        CallerSource => "Caller-Source",
117        CallerTransferSource => "Caller-Transfer-Source",
118        CallerRdnis => "Caller-RDNIS",
119        CallerChannelName => "Caller-Channel-Name",
120        CallerProfileIndex => "Caller-Profile-Index",
121        CallerScreenBit => "Caller-Screen-Bit",
122        CallerPrivacyHideName => "Caller-Privacy-Hide-Name",
123        CallerPrivacyHideNumber => "Caller-Privacy-Hide-Number",
124
125        // --- Other-leg profile (from switch_caller_profile_event_set_data, "Other-Leg" prefix) ---
126        OtherLegDirection => "Other-Leg-Direction",
127        OtherLegLogicalDirection => "Other-Leg-Logical-Direction",
128        OtherLegUsername => "Other-Leg-Username",
129        OtherLegDialplan => "Other-Leg-Dialplan",
130        OtherLegCallerIdName => "Other-Leg-Caller-ID-Name",
131        OtherLegCallerIdNumber => "Other-Leg-Caller-ID-Number",
132        OtherLegOrigCallerIdName => "Other-Leg-Orig-Caller-ID-Name",
133        OtherLegOrigCallerIdNumber => "Other-Leg-Orig-Caller-ID-Number",
134        OtherLegCalleeIdName => "Other-Leg-Callee-ID-Name",
135        OtherLegCalleeIdNumber => "Other-Leg-Callee-ID-Number",
136        OtherLegNetworkAddr => "Other-Leg-Network-Addr",
137        OtherLegAni => "Other-Leg-ANI",
138        OtherLegAniii => "Other-Leg-ANI-II",
139        OtherLegDestinationNumber => "Other-Leg-Destination-Number",
140        OtherLegSource => "Other-Leg-Source",
141        OtherLegTransferSource => "Other-Leg-Transfer-Source",
142        OtherLegContext => "Other-Leg-Context",
143        OtherLegRdnis => "Other-Leg-RDNIS",
144        OtherLegChannelName => "Other-Leg-Channel-Name",
145        OtherLegProfileIndex => "Other-Leg-Profile-Index",
146        OtherLegScreenBit => "Other-Leg-Screen-Bit",
147        OtherLegPrivacyHideName => "Other-Leg-Privacy-Hide-Name",
148        OtherLegPrivacyHideNumber => "Other-Leg-Privacy-Hide-Number",
149
150        // --- Heartbeat (from send_heartbeat in switch_core.c) ---
151        /// Seconds since FreeSWITCH startup.
152        UpTime => "Up-Time",
153        /// Milliseconds since FreeSWITCH startup.
154        UptimeMsec => "Uptime-msec",
155        MaxSessions => "Max-Sessions",
156        SessionPeakMax => "Session-Peak-Max",
157        SessionPeakFiveMin => "Session-Peak-FiveMin",
158        SessionPerSec => "Session-Per-Sec",
159        SessionPerSecFiveMin => "Session-Per-Sec-FiveMin",
160        SessionPerSecMax => "Session-Per-Sec-Max",
161        SessionPerSecLast => "Session-Per-Sec-Last",
162        SessionSinceStartup => "Session-Since-Startup",
163        IdleCpu => "Idle-CPU",
164        HeartbeatInterval => "Heartbeat-Interval",
165        EventInfo => "Event-Info",
166
167        // --- Log (from switch_log_meta_vprintf in switch_log.c) ---
168        LogData => "Log-Data",
169        LogFile => "Log-File",
170        LogFunction => "Log-Function",
171        LogLine => "Log-Line",
172        UserData => "User-Data",
173
174        // --- Application (from switch_core_session_exec in switch_core_session.c) ---
175        ApplicationUuidName => "Application-UUID-Name",
176
177        // --- Sofia event headers (from mod_sofia CUSTOM events) ---
178        Gateway => "Gateway",
179        State => "State",
180        PingStatus => "Ping-Status",
181        Phrase => "Phrase",
182        ProfileName => "profile-name",
183        /// SIP response code (integer) from gateway_state and sip_user_state events.
184        Status => "Status",
185    }
186}
187
188/// Return a lowercase case-alias for `key` when case-insensitive aliasing applies.
189///
190/// The aliasing rule: keys containing `_` (channel variables such as
191/// `variable_*`, and SIP passthrough headers such as `sip_h_*` / `sip_i_*`)
192/// are excluded from case-insensitive aliasing because their suffix encodes
193/// original SIP wire casing that must be preserved verbatim. All other keys
194/// (dash-separated event and framing headers) may be looked up case-insensitively
195/// via their lowercase form.
196///
197/// Returns `Some(key.to_ascii_lowercase())` when aliasing applies, `None` for
198/// underscore-containing keys.
199pub fn case_alias_key(key: &str) -> Option<String> {
200    if key.contains('_') {
201        None
202    } else {
203        Some(key.to_ascii_lowercase())
204    }
205}
206
207/// Normalize a header key to its canonical form for case-insensitive storage.
208///
209/// FreeSWITCH's C ESL uses case-insensitive header lookups (`strcasecmp`), but
210/// stores header names verbatim. Multiple C code paths emit the same logical
211/// header with different casing (e.g. `switch_channel.c` sends `Unique-ID`
212/// while `switch_event.c` sends `unique-id`). This function normalizes keys
213/// so that both resolve to the same `HashMap` entry.
214///
215/// **Strategy:**
216/// 1. Known [`EventHeader`] variants are matched first (case-insensitive) and
217///    returned in their canonical wire form (e.g. `unique-id` → `Unique-ID`).
218/// 2. Keys containing underscores pass through verbatim (see [`case_alias_key`]).
219/// 3. Unknown dash-separated keys are Title-Cased to match FreeSWITCH's
220///    dominant convention for event and framing headers.
221pub fn normalize_header_key(raw: &str) -> String {
222    if let Ok(eh) = raw.parse::<EventHeader>() {
223        return eh
224            .as_str()
225            .to_string();
226    }
227    if case_alias_key(raw).is_none() {
228        raw.to_string()
229    } else {
230        title_case_dashes(raw)
231    }
232}
233
234fn title_case_dashes(s: &str) -> String {
235    let mut result = String::with_capacity(s.len());
236    let mut capitalize_next = true;
237    for c in s.chars() {
238        if c == '-' {
239            result.push('-');
240            capitalize_next = true;
241        } else if capitalize_next {
242            result.push(c.to_ascii_uppercase());
243            capitalize_next = false;
244        } else {
245            result.push(c.to_ascii_lowercase());
246        }
247    }
248    result
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn display_round_trip() {
257        assert_eq!(EventHeader::UniqueId.to_string(), "Unique-ID");
258        assert_eq!(
259            EventHeader::ChannelCallState.to_string(),
260            "Channel-Call-State"
261        );
262        assert_eq!(
263            EventHeader::CallerCallerIdName.to_string(),
264            "Caller-Caller-ID-Name"
265        );
266        assert_eq!(EventHeader::Priority.to_string(), "priority");
267    }
268
269    #[test]
270    fn as_ref_str() {
271        let h: &str = EventHeader::UniqueId.as_ref();
272        assert_eq!(h, "Unique-ID");
273    }
274
275    #[test]
276    fn from_str_case_insensitive() {
277        assert_eq!(
278            "unique-id".parse::<EventHeader>(),
279            Ok(EventHeader::UniqueId)
280        );
281        assert_eq!(
282            "UNIQUE-ID".parse::<EventHeader>(),
283            Ok(EventHeader::UniqueId)
284        );
285        assert_eq!(
286            "Unique-ID".parse::<EventHeader>(),
287            Ok(EventHeader::UniqueId)
288        );
289        assert_eq!(
290            "channel-call-state".parse::<EventHeader>(),
291            Ok(EventHeader::ChannelCallState)
292        );
293    }
294
295    #[test]
296    fn from_str_unknown() {
297        let err = "X-Custom-Not-In-Enum".parse::<EventHeader>();
298        assert!(err.is_err());
299        assert_eq!(
300            err.unwrap_err()
301                .to_string(),
302            "unknown event header: X-Custom-Not-In-Enum"
303        );
304    }
305
306    // --- normalize_header_key tests ---
307    // FreeSWITCH C ESL uses strcasecmp for header lookups but stores names
308    // verbatim. Multiple C code paths emit the same logical header with
309    // different casing (switch_channel.c Title-Case vs switch_event.c lowercase
310    // vs switch_core_codec.c mixed). normalize_header_key canonicalizes keys
311    // so they collapse to a single HashMap entry.
312
313    #[test]
314    fn normalize_known_enum_variants_return_canonical_form() {
315        // EventHeader::from_str is case-insensitive; canonical as_str() is returned
316        assert_eq!(normalize_header_key("unique-id"), "Unique-ID");
317        assert_eq!(normalize_header_key("UNIQUE-ID"), "Unique-ID");
318        assert_eq!(normalize_header_key("Unique-ID"), "Unique-ID");
319        assert_eq!(normalize_header_key("dtmf-digit"), "DTMF-Digit");
320        assert_eq!(normalize_header_key("DTMF-DIGIT"), "DTMF-Digit");
321        assert_eq!(
322            normalize_header_key("channel-call-uuid"),
323            "Channel-Call-UUID"
324        );
325        assert_eq!(normalize_header_key("event-name"), "Event-Name");
326    }
327
328    #[test]
329    fn normalize_known_underscore_variants_return_canonical_form() {
330        // Headers whose canonical form contains underscores
331        assert_eq!(normalize_header_key("priority"), "priority");
332        assert_eq!(normalize_header_key("PRIORITY"), "priority");
333        assert_eq!(normalize_header_key("pl_data"), "pl_data");
334        assert_eq!(normalize_header_key("PL_DATA"), "pl_data");
335        assert_eq!(normalize_header_key("sip_content_type"), "sip_content_type");
336        assert_eq!(normalize_header_key("gateway_name"), "gateway_name");
337        assert_eq!(normalize_header_key("event"), "event");
338        assert_eq!(normalize_header_key("EVENT"), "event");
339    }
340
341    #[test]
342    fn normalize_codec_headers_from_switch_core_codec() {
343        // switch_core_codec.c sends lowercase, switch_channel_event_set_data sends Title-Case
344        // Both must normalize to the canonical EventHeader form
345        assert_eq!(
346            normalize_header_key("channel-read-codec-bit-rate"),
347            "Channel-Read-Codec-Bit-Rate"
348        );
349        assert_eq!(
350            normalize_header_key("Channel-Read-Codec-Bit-Rate"),
351            "Channel-Read-Codec-Bit-Rate"
352        );
353        // switch_core_codec.c mixed case for write: "Channel-Write-codec-bit-rate"
354        assert_eq!(
355            normalize_header_key("Channel-Write-codec-bit-rate"),
356            "Channel-Write-Codec-Bit-Rate"
357        );
358        assert_eq!(
359            normalize_header_key("channel-video-read-codec-name"),
360            "Channel-Video-Read-Codec-Name"
361        );
362    }
363
364    #[test]
365    fn normalize_unknown_underscore_keys_passthrough() {
366        // Channel variables and sip_h_* passthrough preserve original casing
367        assert_eq!(
368            normalize_header_key("variable_sip_call_id"),
369            "variable_sip_call_id"
370        );
371        assert_eq!(
372            normalize_header_key("variable_sip_h_X-My-CUSTOM-Header"),
373            "variable_sip_h_X-My-CUSTOM-Header"
374        );
375        assert_eq!(
376            normalize_header_key("variable_sip_h_Diversion"),
377            "variable_sip_h_Diversion"
378        );
379    }
380
381    #[test]
382    fn normalize_unknown_dash_keys_title_case() {
383        // Framing and unknown event headers get Title-Cased
384        assert_eq!(normalize_header_key("content-type"), "Content-Type");
385        assert_eq!(normalize_header_key("Content-Type"), "Content-Type");
386        assert_eq!(normalize_header_key("CONTENT-TYPE"), "Content-Type");
387        assert_eq!(normalize_header_key("x-custom-header"), "X-Custom-Header");
388        assert_eq!(
389            normalize_header_key("Content-Disposition"),
390            "Content-Disposition"
391        );
392        assert_eq!(normalize_header_key("reply-text"), "Reply-Text");
393    }
394
395    #[test]
396    fn normalize_idempotent_for_all_enum_variants() {
397        // Normalizing an already-canonical wire string must return it unchanged
398        let variants = [
399            EventHeader::EventName,
400            EventHeader::UniqueId,
401            EventHeader::ChannelCallUuid,
402            EventHeader::DtmfDigit,
403            EventHeader::Priority,
404            EventHeader::PlData,
405            EventHeader::SipEvent,
406            EventHeader::GatewayName,
407            EventHeader::SipContentType,
408            EventHeader::ChannelReadCodecBitRate,
409            EventHeader::ChannelVideoWriteCodecRate,
410            EventHeader::LogLevel,
411        ];
412        for v in variants {
413            let canonical = v.as_str();
414            assert_eq!(
415                normalize_header_key(canonical),
416                canonical,
417                "normalization not idempotent for {canonical}"
418            );
419        }
420    }
421
422    #[test]
423    fn parse_missing_event_default_headers() {
424        // From switch_event_prep_for_delivery_detailed (switch_event.c)
425        assert!("Event-Date-Local"
426            .parse::<EventHeader>()
427            .is_ok());
428        assert!("Event-Date-GMT"
429            .parse::<EventHeader>()
430            .is_ok());
431        assert!("Event-Date-Timestamp"
432            .parse::<EventHeader>()
433            .is_ok());
434        assert!("Event-Calling-File"
435            .parse::<EventHeader>()
436            .is_ok());
437        assert!("Event-Calling-Function"
438            .parse::<EventHeader>()
439            .is_ok());
440        assert!("Event-Calling-Line-Number"
441            .parse::<EventHeader>()
442            .is_ok());
443        assert!("Event-Sequence"
444            .parse::<EventHeader>()
445            .is_ok());
446    }
447
448    #[test]
449    fn parse_missing_channel_basic_data_headers() {
450        // From switch_channel_event_set_basic_data (switch_channel.c)
451        assert!("Channel-Presence-ID"
452            .parse::<EventHeader>()
453            .is_ok());
454        assert!("Channel-Presence-Data"
455            .parse::<EventHeader>()
456            .is_ok());
457        assert!("Presence-Data-Cols"
458            .parse::<EventHeader>()
459            .is_ok());
460        assert!("Presence-Call-Direction"
461            .parse::<EventHeader>()
462            .is_ok());
463        assert!("Channel-HIT-Dialplan"
464            .parse::<EventHeader>()
465            .is_ok());
466        assert!("Session-External-ID"
467            .parse::<EventHeader>()
468            .is_ok());
469        assert!("Other-Type"
470            .parse::<EventHeader>()
471            .is_ok());
472    }
473
474    #[test]
475    fn parse_missing_callstate_and_dtmf_headers() {
476        // From switch_channel_perform_set_callstate (switch_channel.c)
477        assert!("Channel-Call-State-Number"
478            .parse::<EventHeader>()
479            .is_ok());
480        assert!("Original-Channel-Call-State"
481            .parse::<EventHeader>()
482            .is_ok());
483        // From switch_channel_dequeue_dtmf (switch_channel.c)
484        assert!("DTMF-Duration"
485            .parse::<EventHeader>()
486            .is_ok());
487        assert!("DTMF-Source"
488            .parse::<EventHeader>()
489            .is_ok());
490    }
491
492    #[test]
493    fn parse_missing_caller_profile_headers() {
494        // From switch_caller_profile_event_set_data (switch_caller.c) with "Caller-" prefix
495        assert!("Caller-Logical-Direction"
496            .parse::<EventHeader>()
497            .is_ok());
498        assert!("Caller-Username"
499            .parse::<EventHeader>()
500            .is_ok());
501        assert!("Caller-Dialplan"
502            .parse::<EventHeader>()
503            .is_ok());
504        assert!("Caller-ANI"
505            .parse::<EventHeader>()
506            .is_ok());
507        assert!("Caller-ANI-II"
508            .parse::<EventHeader>()
509            .is_ok());
510        assert!("Caller-Source"
511            .parse::<EventHeader>()
512            .is_ok());
513        assert!("Caller-Transfer-Source"
514            .parse::<EventHeader>()
515            .is_ok());
516        assert!("Caller-RDNIS"
517            .parse::<EventHeader>()
518            .is_ok());
519        assert!("Caller-Channel-Name"
520            .parse::<EventHeader>()
521            .is_ok());
522        assert!("Caller-Profile-Index"
523            .parse::<EventHeader>()
524            .is_ok());
525        assert!("Caller-Screen-Bit"
526            .parse::<EventHeader>()
527            .is_ok());
528        assert!("Caller-Privacy-Hide-Name"
529            .parse::<EventHeader>()
530            .is_ok());
531        assert!("Caller-Privacy-Hide-Number"
532            .parse::<EventHeader>()
533            .is_ok());
534    }
535
536    #[test]
537    fn parse_heartbeat_headers() {
538        assert!("Up-Time"
539            .parse::<EventHeader>()
540            .is_ok());
541        assert!("Uptime-msec"
542            .parse::<EventHeader>()
543            .is_ok());
544        assert!("Max-Sessions"
545            .parse::<EventHeader>()
546            .is_ok());
547        assert!("Session-Peak-Max"
548            .parse::<EventHeader>()
549            .is_ok());
550        assert!("Session-Peak-FiveMin"
551            .parse::<EventHeader>()
552            .is_ok());
553        assert!("Session-Per-Sec"
554            .parse::<EventHeader>()
555            .is_ok());
556        assert!("Session-Per-Sec-FiveMin"
557            .parse::<EventHeader>()
558            .is_ok());
559        assert!("Session-Per-Sec-Max"
560            .parse::<EventHeader>()
561            .is_ok());
562        assert!("Session-Per-Sec-Last"
563            .parse::<EventHeader>()
564            .is_ok());
565        assert!("Session-Since-Startup"
566            .parse::<EventHeader>()
567            .is_ok());
568        assert!("Idle-CPU"
569            .parse::<EventHeader>()
570            .is_ok());
571        assert!("Heartbeat-Interval"
572            .parse::<EventHeader>()
573            .is_ok());
574        assert!("Event-Info"
575            .parse::<EventHeader>()
576            .is_ok());
577    }
578
579    #[test]
580    fn parse_log_headers() {
581        assert!("Log-Data"
582            .parse::<EventHeader>()
583            .is_ok());
584        assert!("Log-File"
585            .parse::<EventHeader>()
586            .is_ok());
587        assert!("Log-Function"
588            .parse::<EventHeader>()
589            .is_ok());
590        assert!("Log-Line"
591            .parse::<EventHeader>()
592            .is_ok());
593        assert!("User-Data"
594            .parse::<EventHeader>()
595            .is_ok());
596    }
597
598    #[test]
599    fn parse_application_uuid_name() {
600        assert!("Application-UUID-Name"
601            .parse::<EventHeader>()
602            .is_ok());
603    }
604
605    #[test]
606    fn parse_sofia_event_headers() {
607        assert_eq!("Gateway".parse::<EventHeader>(), Ok(EventHeader::Gateway));
608        assert_eq!("State".parse::<EventHeader>(), Ok(EventHeader::State));
609        assert_eq!(
610            "Ping-Status".parse::<EventHeader>(),
611            Ok(EventHeader::PingStatus)
612        );
613        assert_eq!("Phrase".parse::<EventHeader>(), Ok(EventHeader::Phrase));
614        assert_eq!(
615            "profile-name".parse::<EventHeader>(),
616            Ok(EventHeader::ProfileName)
617        );
618    }
619
620    #[test]
621    fn parse_missing_other_leg_headers() {
622        // From switch_caller_profile_event_set_data with "Other-Leg" prefix
623        assert!("Other-Leg-Direction"
624            .parse::<EventHeader>()
625            .is_ok());
626        assert!("Other-Leg-Logical-Direction"
627            .parse::<EventHeader>()
628            .is_ok());
629        assert!("Other-Leg-Username"
630            .parse::<EventHeader>()
631            .is_ok());
632        assert!("Other-Leg-Dialplan"
633            .parse::<EventHeader>()
634            .is_ok());
635        assert!("Other-Leg-Caller-ID-Name"
636            .parse::<EventHeader>()
637            .is_ok());
638        assert!("Other-Leg-Caller-ID-Number"
639            .parse::<EventHeader>()
640            .is_ok());
641        assert!("Other-Leg-Orig-Caller-ID-Name"
642            .parse::<EventHeader>()
643            .is_ok());
644        assert!("Other-Leg-Orig-Caller-ID-Number"
645            .parse::<EventHeader>()
646            .is_ok());
647        assert!("Other-Leg-Callee-ID-Name"
648            .parse::<EventHeader>()
649            .is_ok());
650        assert!("Other-Leg-Callee-ID-Number"
651            .parse::<EventHeader>()
652            .is_ok());
653        assert!("Other-Leg-Network-Addr"
654            .parse::<EventHeader>()
655            .is_ok());
656        assert!("Other-Leg-ANI"
657            .parse::<EventHeader>()
658            .is_ok());
659        assert!("Other-Leg-ANI-II"
660            .parse::<EventHeader>()
661            .is_ok());
662        assert!("Other-Leg-Destination-Number"
663            .parse::<EventHeader>()
664            .is_ok());
665        assert!("Other-Leg-Source"
666            .parse::<EventHeader>()
667            .is_ok());
668        assert!("Other-Leg-Transfer-Source"
669            .parse::<EventHeader>()
670            .is_ok());
671        assert!("Other-Leg-Context"
672            .parse::<EventHeader>()
673            .is_ok());
674        assert!("Other-Leg-RDNIS"
675            .parse::<EventHeader>()
676            .is_ok());
677        assert!("Other-Leg-Channel-Name"
678            .parse::<EventHeader>()
679            .is_ok());
680        assert!("Other-Leg-Profile-Index"
681            .parse::<EventHeader>()
682            .is_ok());
683        assert!("Other-Leg-Screen-Bit"
684            .parse::<EventHeader>()
685            .is_ok());
686        assert!("Other-Leg-Privacy-Hide-Name"
687            .parse::<EventHeader>()
688            .is_ok());
689        assert!("Other-Leg-Privacy-Hide-Number"
690            .parse::<EventHeader>()
691            .is_ok());
692    }
693
694    #[test]
695    fn case_alias_key_underscore_excluded() {
696        // Underscore keys are excluded from case-insensitive aliasing.
697        assert_eq!(case_alias_key("variable_foo"), None);
698        assert_eq!(case_alias_key("sip_h_Call-Info"), None);
699        assert_eq!(case_alias_key("sip_i_contact"), None);
700    }
701
702    #[test]
703    fn case_alias_key_dash_keys_lowercased() {
704        // Dash-separated keys return their lowercase alias.
705        assert_eq!(
706            case_alias_key("Content-Type"),
707            Some("content-type".to_string())
708        );
709        assert_eq!(case_alias_key("UNIQUE-ID"), Some("unique-id".to_string()));
710        assert_eq!(case_alias_key("reply-text"), Some("reply-text".to_string()));
711    }
712}