Skip to main content

freeswitch_types/
lookup.rs

1//! Shared trait for typed header lookups from any key-value store.
2//!
3//! [`HeaderLookup`] provides convenience accessors (typed channel state,
4//! call direction, timetable extraction, etc.) to any type that can look up
5//! headers and variables by name. Implement the two required methods and get
6//! everything else for free.
7
8use crate::channel::{
9    AnswerState, CallDirection, CallState, ChannelState, ChannelTimetable, HangupCause,
10    ParseAnswerStateError, ParseCallDirectionError, ParseCallStateError, ParseChannelStateError,
11    ParseHangupCauseError, ParseTimetableError,
12};
13#[cfg(feature = "esl")]
14use crate::event::{EslEventPriority, ParsePriorityError};
15use crate::headers::EventHeader;
16use crate::sofia::{
17    GatewayPingStatus, GatewayRegState, ParseGatewayPingStatusError, ParseGatewayRegStateError,
18    ParseSipUserPingStatusError, ParseSofiaEventSubclassError, SipUserPingStatus,
19    SofiaEventSubclass,
20};
21use crate::variables::VariableName;
22use sip_header::SipHeaderLookup;
23use std::str::FromStr;
24
25/// Parse an optional wire header value into a typed result.
26///
27/// Collapses the common `match self.header(...) { Some(s) => Ok(Some(s.parse()?)),
28/// None => Ok(None) }` pattern used by many `HeaderLookup` accessors.
29#[inline]
30fn parse_opt<T: FromStr>(raw: Option<&str>) -> Result<Option<T>, T::Err> {
31    raw.map(str::parse)
32        .transpose()
33}
34
35/// Trait for looking up ESL headers and channel variables from any key-value store.
36///
37/// Implementors provide two methods -- `header_str(&str)` and `variable_str(&str)` --
38/// and get all typed accessors (`channel_state()`, `call_direction()`, `timetable()`,
39/// etc.) as default implementations.
40///
41/// `HeaderLookup: SipHeaderLookup` — every `HeaderLookup` implementor must also
42/// implement [`sip_header::SipHeaderLookup`] so callers can reach the typed RFC
43/// SIP parsers (`call_info()`, `history_info()`, `geolocation()`) directly from
44/// the same value. For stores that treat ESL headers and SIP headers as the
45/// same flat key-value namespace, a one-line delegation from `sip_header_str`
46/// to `header_str` is the intended impl — see [`EslEvent`](crate::EslEvent)
47/// for the pattern. Stores with FreeSWITCH-specific transport encoding (ARRAY,
48/// `[bracket]` wrapping) should use [`EslHeaders`](crate::EslHeaders), which
49/// overrides the `SipHeaderLookup` parsers to strip those quirks.
50///
51/// This trait must be in scope to call its methods on `EslEvent` -- including
52/// `unique_id()`, `hangup_cause()`, and `channel_state()`. Import it directly
53/// or via the prelude:
54///
55/// ```ignore
56/// use freeswitch_esl_tokio::prelude::*;
57/// // or: use freeswitch_esl_tokio::HeaderLookup;
58/// // or: use freeswitch_types::HeaderLookup;
59/// ```
60///
61/// # Example
62///
63/// ```
64/// use std::collections::HashMap;
65/// use freeswitch_types::{HeaderLookup, EventHeader, ChannelVariable};
66/// use freeswitch_types::sip_header::SipHeaderLookup;
67///
68/// struct MyStore(HashMap<String, String>);
69///
70/// // HeaderLookup has SipHeaderLookup as a supertrait. For stores that
71/// // treat ESL and SIP headers as one flat namespace, delegate
72/// // sip_header_str to the same lookup.
73/// impl SipHeaderLookup for MyStore {
74///     fn sip_header_str(&self, name: &str) -> Option<&str> {
75///         self.0.get(name).map(|s| s.as_str())
76///     }
77/// }
78///
79/// impl HeaderLookup for MyStore {
80///     fn header_str(&self, name: &str) -> Option<&str> {
81///         self.0.get(name).map(|s| s.as_str())
82///     }
83///     fn variable_str(&self, name: &str) -> Option<&str> {
84///         self.0.get(&format!("variable_{}", name)).map(|s| s.as_str())
85///     }
86/// }
87///
88/// let mut map = HashMap::new();
89/// map.insert("Channel-State".into(), "CS_EXECUTE".into());
90/// map.insert("variable_read_codec".into(), "PCMU".into());
91/// let store = MyStore(map);
92///
93/// // Typed accessor from the trait (returns Result<Option<T>, E>):
94/// assert!(store.channel_state().unwrap().is_some());
95///
96/// // Enum-based lookups:
97/// assert_eq!(store.header(EventHeader::ChannelState), Some("CS_EXECUTE"));
98/// assert_eq!(store.variable(ChannelVariable::ReadCodec), Some("PCMU"));
99/// ```
100pub trait HeaderLookup: SipHeaderLookup {
101    /// Look up a header by its raw wire name (e.g. `"Unique-ID"`).
102    fn header_str(&self, name: &str) -> Option<&str>;
103
104    /// Look up a channel variable by its bare name (e.g. `"sip_call_id"`).
105    ///
106    /// Implementations typically prepend `variable_` and delegate to `header_str`.
107    fn variable_str(&self, name: &str) -> Option<&str>;
108
109    /// Look up a header by its [`EventHeader`] enum variant.
110    fn header(&self, name: EventHeader) -> Option<&str> {
111        self.header_str(name.as_str())
112    }
113
114    /// Look up a channel variable by its typed enum variant.
115    fn variable(&self, name: impl VariableName) -> Option<&str> {
116        self.variable_str(name.as_str())
117    }
118
119    /// `Unique-ID` header, falling back to `Caller-Unique-ID`.
120    fn unique_id(&self) -> Option<&str> {
121        self.header(EventHeader::UniqueId)
122            .or_else(|| self.header(EventHeader::CallerUniqueId))
123    }
124
125    /// `Job-UUID` header from `bgapi` `BACKGROUND_JOB` events.
126    ///
127    /// The only thing separating your job's result from another ESL client's:
128    /// `BACKGROUND_JOB` is fired globally, so seeing one you did not issue is
129    /// routine rather than exceptional.
130    fn job_uuid(&self) -> Option<&str> {
131        self.header(EventHeader::JobUuid)
132    }
133
134    /// `Channel-Name` header (e.g. `sofia/internal/1000@domain`).
135    fn channel_name(&self) -> Option<&str> {
136        self.header(EventHeader::ChannelName)
137    }
138
139    /// `Caller-Caller-ID-Number` header.
140    fn caller_id_number(&self) -> Option<&str> {
141        self.header(EventHeader::CallerCallerIdNumber)
142    }
143
144    /// `Caller-Caller-ID-Name` header.
145    fn caller_id_name(&self) -> Option<&str> {
146        self.header(EventHeader::CallerCallerIdName)
147    }
148
149    /// `Caller-Destination-Number` header.
150    fn destination_number(&self) -> Option<&str> {
151        self.header(EventHeader::CallerDestinationNumber)
152    }
153
154    /// `Caller-Callee-ID-Number` header.
155    fn callee_id_number(&self) -> Option<&str> {
156        self.header(EventHeader::CallerCalleeIdNumber)
157    }
158
159    /// `Caller-Callee-ID-Name` header.
160    fn callee_id_name(&self) -> Option<&str> {
161        self.header(EventHeader::CallerCalleeIdName)
162    }
163
164    /// `Channel-Presence-ID` header (e.g. `1000@example.com`).
165    fn channel_presence_id(&self) -> Option<&str> {
166        self.header(EventHeader::ChannelPresenceId)
167    }
168
169    /// `Presence-Call-Direction` header, parsed into a [`CallDirection`].
170    fn presence_call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
171        parse_opt(self.header(EventHeader::PresenceCallDirection))
172    }
173
174    /// `Event-Date-Timestamp` header (microseconds since epoch).
175    fn event_date_timestamp(&self) -> Option<&str> {
176        self.header(EventHeader::EventDateTimestamp)
177    }
178
179    /// `Event-Sequence` header (sequential event counter).
180    fn event_sequence(&self) -> Option<&str> {
181        self.header(EventHeader::EventSequence)
182    }
183
184    /// `DTMF-Duration` header (digit duration in milliseconds).
185    fn dtmf_duration(&self) -> Option<&str> {
186        self.header(EventHeader::DtmfDuration)
187    }
188
189    /// `DTMF-Source` header (e.g. `rtp`, `inband`).
190    fn dtmf_source(&self) -> Option<&str> {
191        self.header(EventHeader::DtmfSource)
192    }
193
194    /// Parse the `Hangup-Cause` header into a [`HangupCause`].
195    ///
196    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
197    fn hangup_cause(&self) -> Result<Option<HangupCause>, ParseHangupCauseError> {
198        parse_opt(self.header(EventHeader::HangupCause))
199    }
200
201    /// `Event-Subclass` header for `CUSTOM` events (e.g. `sofia::register`).
202    fn event_subclass(&self) -> Option<&str> {
203        self.header(EventHeader::EventSubclass)
204    }
205
206    /// Parse `Event-Subclass` as a typed [`SofiaEventSubclass`].
207    ///
208    /// Returns `Ok(None)` if the header is absent, `Err` if present but not a
209    /// recognized `sofia::*` subclass.
210    fn sofia_event_subclass(
211        &self,
212    ) -> Result<Option<SofiaEventSubclass>, ParseSofiaEventSubclassError> {
213        parse_opt(self.event_subclass())
214    }
215
216    /// `Gateway` header from `sofia::gateway_state` / `sofia::gateway_add` events.
217    fn gateway(&self) -> Option<&str> {
218        self.header(EventHeader::Gateway)
219    }
220
221    /// `profile-name` header from `sofia::sip_user_state` events.
222    fn profile_name(&self) -> Option<&str> {
223        self.header(EventHeader::ProfileName)
224    }
225
226    /// `Phrase` header (SIP reason phrase) from sofia state events.
227    fn phrase(&self) -> Option<&str> {
228        self.header(EventHeader::Phrase)
229    }
230
231    /// `Status` header (SIP response code) from `sofia::gateway_state` and
232    /// `sofia::sip_user_state` events.
233    fn sip_status_code(&self) -> Result<Option<u16>, std::num::ParseIntError> {
234        parse_opt(self.header(EventHeader::Status))
235    }
236
237    /// Parse the `State` header as a [`GatewayRegState`].
238    ///
239    /// Returns `Ok(None)` if absent, `Err` if present but unparseable.
240    fn gateway_reg_state(&self) -> Result<Option<GatewayRegState>, ParseGatewayRegStateError> {
241        parse_opt(self.header(EventHeader::State))
242    }
243
244    /// Parse `Ping-Status` as a [`GatewayPingStatus`].
245    ///
246    /// Use on `sofia::gateway_state` events. For `sofia::sip_user_state`, use
247    /// [`sip_user_ping_status()`](Self::sip_user_ping_status) instead.
248    fn gateway_ping_status(
249        &self,
250    ) -> Result<Option<GatewayPingStatus>, ParseGatewayPingStatusError> {
251        parse_opt(self.header(EventHeader::PingStatus))
252    }
253
254    /// Parse `Ping-Status` as a [`SipUserPingStatus`].
255    ///
256    /// Use on `sofia::sip_user_state` events. For `sofia::gateway_state`, use
257    /// [`gateway_ping_status()`](Self::gateway_ping_status) instead.
258    fn sip_user_ping_status(
259        &self,
260    ) -> Result<Option<SipUserPingStatus>, ParseSipUserPingStatusError> {
261        parse_opt(self.header(EventHeader::PingStatus))
262    }
263
264    /// `pl_data` header -- SIP NOTIFY body content from `NOTIFY_IN` events.
265    ///
266    /// Contains the JSON payload (already percent-decoded by the ESL parser).
267    /// For NG9-1-1 events this is the inner object without the wrapper key
268    /// (FreeSWITCH strips it).
269    fn pl_data(&self) -> Option<&str> {
270        self.header(EventHeader::PlData)
271    }
272
273    /// `event` header -- SIP event package name from `NOTIFY_IN` events.
274    ///
275    /// Examples: `emergency-AbandonedCall`, `emergency-ServiceState`.
276    fn sip_event(&self) -> Option<&str> {
277        self.header(EventHeader::SipEvent)
278    }
279
280    /// `gateway_name` header -- gateway that received a SIP NOTIFY.
281    fn gateway_name(&self) -> Option<&str> {
282        self.header(EventHeader::GatewayName)
283    }
284
285    /// Parse the `Channel-State` header into a [`ChannelState`].
286    ///
287    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
288    fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
289        parse_opt(self.header(EventHeader::ChannelState))
290    }
291
292    /// Parse the `Channel-State-Number` header into a [`ChannelState`].
293    ///
294    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
295    fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
296        match self.header(EventHeader::ChannelStateNumber) {
297            Some(s) => {
298                let n: u8 = s
299                    .parse()
300                    .map_err(|_| ParseChannelStateError(s.to_string()))?;
301                ChannelState::from_number(n)
302                    .ok_or_else(|| ParseChannelStateError(s.to_string()))
303                    .map(Some)
304            }
305            None => Ok(None),
306        }
307    }
308
309    /// Parse the `Channel-Call-State` header into a [`CallState`].
310    ///
311    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
312    fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
313        parse_opt(self.header(EventHeader::ChannelCallState))
314    }
315
316    /// Parse the `Answer-State` header into an [`AnswerState`].
317    ///
318    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
319    fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
320        parse_opt(self.header(EventHeader::AnswerState))
321    }
322
323    /// Parse the `Call-Direction` header into a [`CallDirection`].
324    ///
325    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
326    fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
327        parse_opt(self.header(EventHeader::CallDirection))
328    }
329
330    /// Parse the `priority` header value.
331    ///
332    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
333    #[cfg(feature = "esl")]
334    fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
335        parse_opt(self.header(EventHeader::Priority))
336    }
337
338    /// Extract timetable from timestamp headers with the given prefix.
339    ///
340    /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
341    /// Returns `Err` if a header is present but contains an invalid value.
342    fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
343        ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
344    }
345
346    /// Caller-leg channel timetable (`Caller-*-Time` headers).
347    fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
348        self.timetable("Caller")
349    }
350
351    /// Other-leg channel timetable (`Other-Leg-*-Time` headers).
352    fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
353        self.timetable("Other-Leg")
354    }
355}
356
357impl HeaderLookup for std::collections::HashMap<String, String> {
358    fn header_str(&self, name: &str) -> Option<&str> {
359        self.get(name)
360            .map(|s| s.as_str())
361    }
362
363    fn variable_str(&self, name: &str) -> Option<&str> {
364        self.get(&format!("variable_{name}"))
365            .map(|s| s.as_str())
366    }
367}
368
369// No blanket `HeaderLookup` / `SipHeaderLookup` on `indexmap::IndexMap<String,
370// String>` — both traits are external, so the orphan rules forbid the pair.
371// Wrap in [`EslHeaders`](crate::EslHeaders) instead, which is what callers
372// actually want: ARRAY-aware parsing, `variable_` prefix handling, and
373// bracket stripping are only correct for ESL-sourced headers anyway.
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::variables::ChannelVariable;
379    use std::collections::HashMap;
380
381    struct TestStore(HashMap<String, String>);
382
383    impl SipHeaderLookup for TestStore {
384        fn sip_header_str(&self, name: &str) -> Option<&str> {
385            self.0
386                .get(name)
387                .map(|s| s.as_str())
388        }
389    }
390
391    impl HeaderLookup for TestStore {
392        fn header_str(&self, name: &str) -> Option<&str> {
393            self.0
394                .get(name)
395                .map(|s| s.as_str())
396        }
397        fn variable_str(&self, name: &str) -> Option<&str> {
398            self.0
399                .get(&format!("variable_{}", name))
400                .map(|s| s.as_str())
401        }
402    }
403
404    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
405        let map: HashMap<String, String> = pairs
406            .iter()
407            .map(|(k, v)| (k.to_string(), v.to_string()))
408            .collect();
409        TestStore(map)
410    }
411
412    #[test]
413    fn header_str_direct() {
414        let s = store_with(&[("Unique-ID", "abc-123")]);
415        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
416        assert_eq!(s.header_str("Missing"), None);
417    }
418
419    #[test]
420    fn header_by_enum() {
421        let s = store_with(&[("Unique-ID", "abc-123")]);
422        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
423    }
424
425    #[test]
426    fn variable_str_direct() {
427        let s = store_with(&[("variable_read_codec", "PCMU")]);
428        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
429        assert_eq!(s.variable_str("missing"), None);
430    }
431
432    #[test]
433    fn variable_by_enum() {
434        let s = store_with(&[("variable_read_codec", "PCMU")]);
435        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
436    }
437
438    #[test]
439    fn unique_id_primary() {
440        let s = store_with(&[("Unique-ID", "uuid-1")]);
441        assert_eq!(s.unique_id(), Some("uuid-1"));
442    }
443
444    #[test]
445    fn unique_id_fallback() {
446        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
447        assert_eq!(s.unique_id(), Some("uuid-2"));
448    }
449
450    #[test]
451    fn unique_id_none() {
452        let s = store_with(&[]);
453        assert_eq!(s.unique_id(), None);
454    }
455
456    #[test]
457    fn job_uuid() {
458        let s = store_with(&[("Job-UUID", "job-1")]);
459        assert_eq!(s.job_uuid(), Some("job-1"));
460    }
461
462    #[test]
463    fn channel_name() {
464        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
465        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
466    }
467
468    #[test]
469    fn caller_id_number_and_name() {
470        let s = store_with(&[
471            ("Caller-Caller-ID-Number", "1000"),
472            ("Caller-Caller-ID-Name", "Alice"),
473        ]);
474        assert_eq!(s.caller_id_number(), Some("1000"));
475        assert_eq!(s.caller_id_name(), Some("Alice"));
476    }
477
478    #[test]
479    fn hangup_cause_typed() {
480        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
481        assert_eq!(
482            s.hangup_cause()
483                .unwrap(),
484            Some(crate::channel::HangupCause::NormalClearing)
485        );
486    }
487
488    #[test]
489    fn hangup_cause_invalid_is_error() {
490        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
491        assert!(s
492            .hangup_cause()
493            .is_err());
494    }
495
496    #[test]
497    fn destination_number() {
498        let s = store_with(&[("Caller-Destination-Number", "1000")]);
499        assert_eq!(s.destination_number(), Some("1000"));
500    }
501
502    #[test]
503    fn callee_id() {
504        let s = store_with(&[
505            ("Caller-Callee-ID-Number", "2000"),
506            ("Caller-Callee-ID-Name", "Bob"),
507        ]);
508        assert_eq!(s.callee_id_number(), Some("2000"));
509        assert_eq!(s.callee_id_name(), Some("Bob"));
510    }
511
512    #[test]
513    fn event_subclass() {
514        let s = store_with(&[("Event-Subclass", "sofia::register")]);
515        assert_eq!(s.event_subclass(), Some("sofia::register"));
516    }
517
518    #[test]
519    fn sofia_event_subclass_typed() {
520        let s = store_with(&[("Event-Subclass", "sofia::gateway_state")]);
521        assert_eq!(
522            s.sofia_event_subclass()
523                .unwrap(),
524            Some(crate::sofia::SofiaEventSubclass::GatewayState)
525        );
526    }
527
528    #[test]
529    fn sofia_event_subclass_absent() {
530        let s = store_with(&[]);
531        assert_eq!(
532            s.sofia_event_subclass()
533                .unwrap(),
534            None
535        );
536    }
537
538    #[test]
539    fn sofia_event_subclass_non_sofia_is_error() {
540        let s = store_with(&[("Event-Subclass", "conference::maintenance")]);
541        assert!(s
542            .sofia_event_subclass()
543            .is_err());
544    }
545
546    #[test]
547    fn gateway_reg_state_typed() {
548        let s = store_with(&[("State", "REGED")]);
549        assert_eq!(
550            s.gateway_reg_state()
551                .unwrap(),
552            Some(crate::sofia::GatewayRegState::Reged)
553        );
554    }
555
556    #[test]
557    fn gateway_reg_state_invalid_is_error() {
558        let s = store_with(&[("State", "BOGUS")]);
559        assert!(s
560            .gateway_reg_state()
561            .is_err());
562    }
563
564    #[test]
565    fn gateway_ping_status_typed() {
566        let s = store_with(&[("Ping-Status", "UP")]);
567        assert_eq!(
568            s.gateway_ping_status()
569                .unwrap(),
570            Some(crate::sofia::GatewayPingStatus::Up)
571        );
572    }
573
574    #[test]
575    fn sip_user_ping_status_typed() {
576        let s = store_with(&[("Ping-Status", "REACHABLE")]);
577        assert_eq!(
578            s.sip_user_ping_status()
579                .unwrap(),
580            Some(crate::sofia::SipUserPingStatus::Reachable)
581        );
582    }
583
584    #[test]
585    fn gateway_accessor() {
586        let s = store_with(&[("Gateway", "my-gateway")]);
587        assert_eq!(s.gateway(), Some("my-gateway"));
588    }
589
590    #[test]
591    fn profile_name_accessor() {
592        let s = store_with(&[("profile-name", "internal")]);
593        assert_eq!(s.profile_name(), Some("internal"));
594    }
595
596    #[test]
597    fn phrase_accessor() {
598        let s = store_with(&[("Phrase", "OK")]);
599        assert_eq!(s.phrase(), Some("OK"));
600    }
601
602    #[test]
603    fn channel_state_typed() {
604        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
605        assert_eq!(
606            s.channel_state()
607                .unwrap(),
608            Some(ChannelState::CsExecute)
609        );
610    }
611
612    #[test]
613    fn channel_state_number_typed() {
614        let s = store_with(&[("Channel-State-Number", "4")]);
615        assert_eq!(
616            s.channel_state_number()
617                .unwrap(),
618            Some(ChannelState::CsExecute)
619        );
620    }
621
622    #[test]
623    fn call_state_typed() {
624        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
625        assert_eq!(
626            s.call_state()
627                .unwrap(),
628            Some(CallState::Active)
629        );
630    }
631
632    #[test]
633    fn answer_state_typed() {
634        let s = store_with(&[("Answer-State", "answered")]);
635        assert_eq!(
636            s.answer_state()
637                .unwrap(),
638            Some(AnswerState::Answered)
639        );
640    }
641
642    #[test]
643    fn call_direction_typed() {
644        let s = store_with(&[("Call-Direction", "inbound")]);
645        assert_eq!(
646            s.call_direction()
647                .unwrap(),
648            Some(CallDirection::Inbound)
649        );
650    }
651
652    #[test]
653    fn priority_typed() {
654        let s = store_with(&[("priority", "HIGH")]);
655        assert_eq!(
656            s.priority()
657                .unwrap(),
658            Some(EslEventPriority::High)
659        );
660    }
661
662    #[test]
663    fn timetable_extraction() {
664        let s = store_with(&[
665            ("Caller-Channel-Created-Time", "1700000001000000"),
666            ("Caller-Channel-Answered-Time", "1700000005000000"),
667        ]);
668        let tt = s
669            .caller_timetable()
670            .unwrap()
671            .expect("should have timetable");
672        assert_eq!(tt.created, Some(1700000001000000));
673        assert_eq!(tt.answered, Some(1700000005000000));
674        assert_eq!(tt.hungup, None);
675    }
676
677    #[test]
678    fn timetable_other_leg() {
679        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
680        let tt = s
681            .other_leg_timetable()
682            .unwrap()
683            .expect("should have timetable");
684        assert_eq!(tt.created, Some(1700000001000000));
685    }
686
687    #[test]
688    fn timetable_none_when_absent() {
689        let s = store_with(&[]);
690        assert_eq!(
691            s.caller_timetable()
692                .unwrap(),
693            None
694        );
695    }
696
697    #[test]
698    fn timetable_invalid_is_error() {
699        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
700        let err = s
701            .caller_timetable()
702            .unwrap_err();
703        assert_eq!(err.header, "Caller-Channel-Created-Time");
704    }
705
706    #[test]
707    fn missing_headers_return_none() {
708        let s = store_with(&[]);
709        assert_eq!(
710            s.channel_state()
711                .unwrap(),
712            None
713        );
714        assert_eq!(
715            s.channel_state_number()
716                .unwrap(),
717            None
718        );
719        assert_eq!(
720            s.call_state()
721                .unwrap(),
722            None
723        );
724        assert_eq!(
725            s.answer_state()
726                .unwrap(),
727            None
728        );
729        assert_eq!(
730            s.call_direction()
731                .unwrap(),
732            None
733        );
734        assert_eq!(
735            s.priority()
736                .unwrap(),
737            None
738        );
739        assert_eq!(
740            s.hangup_cause()
741                .unwrap(),
742            None
743        );
744        assert_eq!(s.channel_name(), None);
745        assert_eq!(s.caller_id_number(), None);
746        assert_eq!(s.caller_id_name(), None);
747        assert_eq!(s.destination_number(), None);
748        assert_eq!(s.callee_id_number(), None);
749        assert_eq!(s.callee_id_name(), None);
750        assert_eq!(s.event_subclass(), None);
751        assert_eq!(s.job_uuid(), None);
752        assert_eq!(s.pl_data(), None);
753        assert_eq!(s.sip_event(), None);
754        assert_eq!(s.gateway_name(), None);
755        assert_eq!(s.channel_presence_id(), None);
756        assert_eq!(
757            s.presence_call_direction()
758                .unwrap(),
759            None
760        );
761        assert_eq!(s.event_date_timestamp(), None);
762        assert_eq!(s.event_sequence(), None);
763        assert_eq!(s.dtmf_duration(), None);
764        assert_eq!(s.dtmf_source(), None);
765    }
766
767    #[test]
768    fn notify_in_headers() {
769        let s = store_with(&[
770            ("pl_data", r#"{"invite":"INVITE ..."}"#),
771            ("event", "emergency-AbandonedCall"),
772            ("gateway_name", "ng911-bcf"),
773        ]);
774        assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
775        assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
776        assert_eq!(s.gateway_name(), Some("ng911-bcf"));
777    }
778
779    #[test]
780    fn channel_presence_id() {
781        let s = store_with(&[("Channel-Presence-ID", "1000@example.com")]);
782        assert_eq!(s.channel_presence_id(), Some("1000@example.com"));
783    }
784
785    #[test]
786    fn presence_call_direction_typed() {
787        let s = store_with(&[("Presence-Call-Direction", "outbound")]);
788        assert_eq!(
789            s.presence_call_direction()
790                .unwrap(),
791            Some(CallDirection::Outbound)
792        );
793    }
794
795    #[test]
796    fn event_date_timestamp() {
797        let s = store_with(&[("Event-Date-Timestamp", "1700000001000000")]);
798        assert_eq!(s.event_date_timestamp(), Some("1700000001000000"));
799    }
800
801    #[test]
802    fn event_sequence() {
803        let s = store_with(&[("Event-Sequence", "12345")]);
804        assert_eq!(s.event_sequence(), Some("12345"));
805    }
806
807    #[test]
808    fn dtmf_duration() {
809        let s = store_with(&[("DTMF-Duration", "2000")]);
810        assert_eq!(s.dtmf_duration(), Some("2000"));
811    }
812
813    #[test]
814    fn dtmf_source() {
815        let s = store_with(&[("DTMF-Source", "rtp")]);
816        assert_eq!(s.dtmf_source(), Some("rtp"));
817    }
818
819    #[test]
820    fn invalid_values_return_err() {
821        let s = store_with(&[
822            ("Channel-State", "BOGUS"),
823            ("Channel-State-Number", "999"),
824            ("Channel-Call-State", "BOGUS"),
825            ("Answer-State", "bogus"),
826            ("Call-Direction", "bogus"),
827            ("Presence-Call-Direction", "bogus"),
828            ("priority", "BOGUS"),
829            ("Hangup-Cause", "BOGUS"),
830        ]);
831        assert!(s
832            .channel_state()
833            .is_err());
834        assert!(s
835            .channel_state_number()
836            .is_err());
837        assert!(s
838            .call_state()
839            .is_err());
840        assert!(s
841            .answer_state()
842            .is_err());
843        assert!(s
844            .call_direction()
845            .is_err());
846        assert!(s
847            .presence_call_direction()
848            .is_err());
849        assert!(s
850            .priority()
851            .is_err());
852        assert!(s
853            .hangup_cause()
854            .is_err());
855    }
856}