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