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    /// `Status` header (SIP response code) from `sofia::gateway_state` and
205    /// `sofia::sip_user_state` events.
206    fn sip_status_code(&self) -> Result<Option<u16>, std::num::ParseIntError> {
207        match self.header(EventHeader::Status) {
208            Some(s) => Ok(Some(s.parse()?)),
209            None => Ok(None),
210        }
211    }
212
213    /// Parse the `State` header as a [`GatewayRegState`].
214    ///
215    /// Returns `Ok(None)` if absent, `Err` if present but unparseable.
216    fn gateway_reg_state(&self) -> Result<Option<GatewayRegState>, ParseGatewayRegStateError> {
217        match self.header(EventHeader::State) {
218            Some(s) => Ok(Some(s.parse()?)),
219            None => Ok(None),
220        }
221    }
222
223    /// Parse `Ping-Status` as a [`GatewayPingStatus`].
224    ///
225    /// Use on `sofia::gateway_state` events. For `sofia::sip_user_state`, use
226    /// [`sip_user_ping_status()`](Self::sip_user_ping_status) instead.
227    fn gateway_ping_status(
228        &self,
229    ) -> Result<Option<GatewayPingStatus>, ParseGatewayPingStatusError> {
230        match self.header(EventHeader::PingStatus) {
231            Some(s) => Ok(Some(s.parse()?)),
232            None => Ok(None),
233        }
234    }
235
236    /// Parse `Ping-Status` as a [`SipUserPingStatus`].
237    ///
238    /// Use on `sofia::sip_user_state` events. For `sofia::gateway_state`, use
239    /// [`gateway_ping_status()`](Self::gateway_ping_status) instead.
240    fn sip_user_ping_status(
241        &self,
242    ) -> Result<Option<SipUserPingStatus>, ParseSipUserPingStatusError> {
243        match self.header(EventHeader::PingStatus) {
244            Some(s) => Ok(Some(s.parse()?)),
245            None => Ok(None),
246        }
247    }
248
249    /// `pl_data` header -- SIP NOTIFY body content from `NOTIFY_IN` events.
250    ///
251    /// Contains the JSON payload (already percent-decoded by the ESL parser).
252    /// For NG9-1-1 events this is the inner object without the wrapper key
253    /// (FreeSWITCH strips it).
254    fn pl_data(&self) -> Option<&str> {
255        self.header(EventHeader::PlData)
256    }
257
258    /// `event` header -- SIP event package name from `NOTIFY_IN` events.
259    ///
260    /// Examples: `emergency-AbandonedCall`, `emergency-ServiceState`.
261    fn sip_event(&self) -> Option<&str> {
262        self.header(EventHeader::SipEvent)
263    }
264
265    /// `gateway_name` header -- gateway that received a SIP NOTIFY.
266    fn gateway_name(&self) -> Option<&str> {
267        self.header(EventHeader::GatewayName)
268    }
269
270    /// Parse the `Channel-State` header into a [`ChannelState`].
271    ///
272    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
273    fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
274        match self.header(EventHeader::ChannelState) {
275            Some(s) => Ok(Some(s.parse()?)),
276            None => Ok(None),
277        }
278    }
279
280    /// Parse the `Channel-State-Number` header into a [`ChannelState`].
281    ///
282    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
283    fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
284        match self.header(EventHeader::ChannelStateNumber) {
285            Some(s) => {
286                let n: u8 = s
287                    .parse()
288                    .map_err(|_| ParseChannelStateError(s.to_string()))?;
289                ChannelState::from_number(n)
290                    .ok_or_else(|| ParseChannelStateError(s.to_string()))
291                    .map(Some)
292            }
293            None => Ok(None),
294        }
295    }
296
297    /// Parse the `Channel-Call-State` header into a [`CallState`].
298    ///
299    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
300    fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
301        match self.header(EventHeader::ChannelCallState) {
302            Some(s) => Ok(Some(s.parse()?)),
303            None => Ok(None),
304        }
305    }
306
307    /// Parse the `Answer-State` header into an [`AnswerState`].
308    ///
309    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
310    fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
311        match self.header(EventHeader::AnswerState) {
312            Some(s) => Ok(Some(s.parse()?)),
313            None => Ok(None),
314        }
315    }
316
317    /// Parse the `Call-Direction` header into a [`CallDirection`].
318    ///
319    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
320    fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
321        match self.header(EventHeader::CallDirection) {
322            Some(s) => Ok(Some(s.parse()?)),
323            None => Ok(None),
324        }
325    }
326
327    /// Parse the `priority` header value.
328    ///
329    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
330    #[cfg(feature = "esl")]
331    fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
332        match self.header(EventHeader::Priority) {
333            Some(s) => Ok(Some(s.parse()?)),
334            None => Ok(None),
335        }
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#[cfg(feature = "esl")]
370impl HeaderLookup for indexmap::IndexMap<String, String> {
371    fn header_str(&self, name: &str) -> Option<&str> {
372        self.get(name)
373            .map(|s| s.as_str())
374    }
375
376    fn variable_str(&self, name: &str) -> Option<&str> {
377        self.get(&format!("variable_{name}"))
378            .map(|s| s.as_str())
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::variables::ChannelVariable;
386    use std::collections::HashMap;
387
388    struct TestStore(HashMap<String, String>);
389
390    impl HeaderLookup for TestStore {
391        fn header_str(&self, name: &str) -> Option<&str> {
392            self.0
393                .get(name)
394                .map(|s| s.as_str())
395        }
396        fn variable_str(&self, name: &str) -> Option<&str> {
397            self.0
398                .get(&format!("variable_{}", name))
399                .map(|s| s.as_str())
400        }
401    }
402
403    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
404        let map: HashMap<String, String> = pairs
405            .iter()
406            .map(|(k, v)| (k.to_string(), v.to_string()))
407            .collect();
408        TestStore(map)
409    }
410
411    #[test]
412    fn header_str_direct() {
413        let s = store_with(&[("Unique-ID", "abc-123")]);
414        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
415        assert_eq!(s.header_str("Missing"), None);
416    }
417
418    #[test]
419    fn header_by_enum() {
420        let s = store_with(&[("Unique-ID", "abc-123")]);
421        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
422    }
423
424    #[test]
425    fn variable_str_direct() {
426        let s = store_with(&[("variable_read_codec", "PCMU")]);
427        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
428        assert_eq!(s.variable_str("missing"), None);
429    }
430
431    #[test]
432    fn variable_by_enum() {
433        let s = store_with(&[("variable_read_codec", "PCMU")]);
434        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
435    }
436
437    #[test]
438    fn unique_id_primary() {
439        let s = store_with(&[("Unique-ID", "uuid-1")]);
440        assert_eq!(s.unique_id(), Some("uuid-1"));
441    }
442
443    #[test]
444    fn unique_id_fallback() {
445        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
446        assert_eq!(s.unique_id(), Some("uuid-2"));
447    }
448
449    #[test]
450    fn unique_id_none() {
451        let s = store_with(&[]);
452        assert_eq!(s.unique_id(), None);
453    }
454
455    #[test]
456    fn job_uuid() {
457        let s = store_with(&[("Job-UUID", "job-1")]);
458        assert_eq!(s.job_uuid(), Some("job-1"));
459    }
460
461    #[test]
462    fn channel_name() {
463        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
464        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
465    }
466
467    #[test]
468    fn caller_id_number_and_name() {
469        let s = store_with(&[
470            ("Caller-Caller-ID-Number", "1000"),
471            ("Caller-Caller-ID-Name", "Alice"),
472        ]);
473        assert_eq!(s.caller_id_number(), Some("1000"));
474        assert_eq!(s.caller_id_name(), Some("Alice"));
475    }
476
477    #[test]
478    fn hangup_cause_typed() {
479        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
480        assert_eq!(
481            s.hangup_cause()
482                .unwrap(),
483            Some(crate::channel::HangupCause::NormalClearing)
484        );
485    }
486
487    #[test]
488    fn hangup_cause_invalid_is_error() {
489        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
490        assert!(s
491            .hangup_cause()
492            .is_err());
493    }
494
495    #[test]
496    fn destination_number() {
497        let s = store_with(&[("Caller-Destination-Number", "1000")]);
498        assert_eq!(s.destination_number(), Some("1000"));
499    }
500
501    #[test]
502    fn callee_id() {
503        let s = store_with(&[
504            ("Caller-Callee-ID-Number", "2000"),
505            ("Caller-Callee-ID-Name", "Bob"),
506        ]);
507        assert_eq!(s.callee_id_number(), Some("2000"));
508        assert_eq!(s.callee_id_name(), Some("Bob"));
509    }
510
511    #[test]
512    fn event_subclass() {
513        let s = store_with(&[("Event-Subclass", "sofia::register")]);
514        assert_eq!(s.event_subclass(), Some("sofia::register"));
515    }
516
517    #[test]
518    fn sofia_event_subclass_typed() {
519        let s = store_with(&[("Event-Subclass", "sofia::gateway_state")]);
520        assert_eq!(
521            s.sofia_event_subclass()
522                .unwrap(),
523            Some(crate::sofia::SofiaEventSubclass::GatewayState)
524        );
525    }
526
527    #[test]
528    fn sofia_event_subclass_absent() {
529        let s = store_with(&[]);
530        assert_eq!(
531            s.sofia_event_subclass()
532                .unwrap(),
533            None
534        );
535    }
536
537    #[test]
538    fn sofia_event_subclass_non_sofia_is_error() {
539        let s = store_with(&[("Event-Subclass", "conference::maintenance")]);
540        assert!(s
541            .sofia_event_subclass()
542            .is_err());
543    }
544
545    #[test]
546    fn gateway_reg_state_typed() {
547        let s = store_with(&[("State", "REGED")]);
548        assert_eq!(
549            s.gateway_reg_state()
550                .unwrap(),
551            Some(crate::sofia::GatewayRegState::Reged)
552        );
553    }
554
555    #[test]
556    fn gateway_reg_state_invalid_is_error() {
557        let s = store_with(&[("State", "BOGUS")]);
558        assert!(s
559            .gateway_reg_state()
560            .is_err());
561    }
562
563    #[test]
564    fn gateway_ping_status_typed() {
565        let s = store_with(&[("Ping-Status", "UP")]);
566        assert_eq!(
567            s.gateway_ping_status()
568                .unwrap(),
569            Some(crate::sofia::GatewayPingStatus::Up)
570        );
571    }
572
573    #[test]
574    fn sip_user_ping_status_typed() {
575        let s = store_with(&[("Ping-Status", "REACHABLE")]);
576        assert_eq!(
577            s.sip_user_ping_status()
578                .unwrap(),
579            Some(crate::sofia::SipUserPingStatus::Reachable)
580        );
581    }
582
583    #[test]
584    fn gateway_accessor() {
585        let s = store_with(&[("Gateway", "my-gateway")]);
586        assert_eq!(s.gateway(), Some("my-gateway"));
587    }
588
589    #[test]
590    fn profile_name_accessor() {
591        let s = store_with(&[("profile-name", "internal")]);
592        assert_eq!(s.profile_name(), Some("internal"));
593    }
594
595    #[test]
596    fn phrase_accessor() {
597        let s = store_with(&[("Phrase", "OK")]);
598        assert_eq!(s.phrase(), Some("OK"));
599    }
600
601    #[test]
602    fn channel_state_typed() {
603        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
604        assert_eq!(
605            s.channel_state()
606                .unwrap(),
607            Some(ChannelState::CsExecute)
608        );
609    }
610
611    #[test]
612    fn channel_state_number_typed() {
613        let s = store_with(&[("Channel-State-Number", "4")]);
614        assert_eq!(
615            s.channel_state_number()
616                .unwrap(),
617            Some(ChannelState::CsExecute)
618        );
619    }
620
621    #[test]
622    fn call_state_typed() {
623        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
624        assert_eq!(
625            s.call_state()
626                .unwrap(),
627            Some(CallState::Active)
628        );
629    }
630
631    #[test]
632    fn answer_state_typed() {
633        let s = store_with(&[("Answer-State", "answered")]);
634        assert_eq!(
635            s.answer_state()
636                .unwrap(),
637            Some(AnswerState::Answered)
638        );
639    }
640
641    #[test]
642    fn call_direction_typed() {
643        let s = store_with(&[("Call-Direction", "inbound")]);
644        assert_eq!(
645            s.call_direction()
646                .unwrap(),
647            Some(CallDirection::Inbound)
648        );
649    }
650
651    #[test]
652    fn priority_typed() {
653        let s = store_with(&[("priority", "HIGH")]);
654        assert_eq!(
655            s.priority()
656                .unwrap(),
657            Some(EslEventPriority::High)
658        );
659    }
660
661    #[test]
662    fn timetable_extraction() {
663        let s = store_with(&[
664            ("Caller-Channel-Created-Time", "1700000001000000"),
665            ("Caller-Channel-Answered-Time", "1700000005000000"),
666        ]);
667        let tt = s
668            .caller_timetable()
669            .unwrap()
670            .expect("should have timetable");
671        assert_eq!(tt.created, Some(1700000001000000));
672        assert_eq!(tt.answered, Some(1700000005000000));
673        assert_eq!(tt.hungup, None);
674    }
675
676    #[test]
677    fn timetable_other_leg() {
678        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
679        let tt = s
680            .other_leg_timetable()
681            .unwrap()
682            .expect("should have timetable");
683        assert_eq!(tt.created, Some(1700000001000000));
684    }
685
686    #[test]
687    fn timetable_none_when_absent() {
688        let s = store_with(&[]);
689        assert_eq!(
690            s.caller_timetable()
691                .unwrap(),
692            None
693        );
694    }
695
696    #[test]
697    fn timetable_invalid_is_error() {
698        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
699        let err = s
700            .caller_timetable()
701            .unwrap_err();
702        assert_eq!(err.header, "Caller-Channel-Created-Time");
703    }
704
705    #[test]
706    fn missing_headers_return_none() {
707        let s = store_with(&[]);
708        assert_eq!(
709            s.channel_state()
710                .unwrap(),
711            None
712        );
713        assert_eq!(
714            s.channel_state_number()
715                .unwrap(),
716            None
717        );
718        assert_eq!(
719            s.call_state()
720                .unwrap(),
721            None
722        );
723        assert_eq!(
724            s.answer_state()
725                .unwrap(),
726            None
727        );
728        assert_eq!(
729            s.call_direction()
730                .unwrap(),
731            None
732        );
733        assert_eq!(
734            s.priority()
735                .unwrap(),
736            None
737        );
738        assert_eq!(
739            s.hangup_cause()
740                .unwrap(),
741            None
742        );
743        assert_eq!(s.channel_name(), None);
744        assert_eq!(s.caller_id_number(), None);
745        assert_eq!(s.caller_id_name(), None);
746        assert_eq!(s.destination_number(), None);
747        assert_eq!(s.callee_id_number(), None);
748        assert_eq!(s.callee_id_name(), None);
749        assert_eq!(s.event_subclass(), None);
750        assert_eq!(s.job_uuid(), None);
751        assert_eq!(s.pl_data(), None);
752        assert_eq!(s.sip_event(), None);
753        assert_eq!(s.gateway_name(), None);
754        assert_eq!(s.channel_presence_id(), None);
755        assert_eq!(
756            s.presence_call_direction()
757                .unwrap(),
758            None
759        );
760        assert_eq!(s.event_date_timestamp(), None);
761        assert_eq!(s.event_sequence(), None);
762        assert_eq!(s.dtmf_duration(), None);
763        assert_eq!(s.dtmf_source(), None);
764    }
765
766    #[test]
767    fn notify_in_headers() {
768        let s = store_with(&[
769            ("pl_data", r#"{"invite":"INVITE ..."}"#),
770            ("event", "emergency-AbandonedCall"),
771            ("gateway_name", "ng911-bcf"),
772        ]);
773        assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
774        assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
775        assert_eq!(s.gateway_name(), Some("ng911-bcf"));
776    }
777
778    #[test]
779    fn channel_presence_id() {
780        let s = store_with(&[("Channel-Presence-ID", "1000@example.com")]);
781        assert_eq!(s.channel_presence_id(), Some("1000@example.com"));
782    }
783
784    #[test]
785    fn presence_call_direction_typed() {
786        let s = store_with(&[("Presence-Call-Direction", "outbound")]);
787        assert_eq!(
788            s.presence_call_direction()
789                .unwrap(),
790            Some(CallDirection::Outbound)
791        );
792    }
793
794    #[test]
795    fn event_date_timestamp() {
796        let s = store_with(&[("Event-Date-Timestamp", "1700000001000000")]);
797        assert_eq!(s.event_date_timestamp(), Some("1700000001000000"));
798    }
799
800    #[test]
801    fn event_sequence() {
802        let s = store_with(&[("Event-Sequence", "12345")]);
803        assert_eq!(s.event_sequence(), Some("12345"));
804    }
805
806    #[test]
807    fn dtmf_duration() {
808        let s = store_with(&[("DTMF-Duration", "2000")]);
809        assert_eq!(s.dtmf_duration(), Some("2000"));
810    }
811
812    #[test]
813    fn dtmf_source() {
814        let s = store_with(&[("DTMF-Source", "rtp")]);
815        assert_eq!(s.dtmf_source(), Some("rtp"));
816    }
817
818    #[test]
819    fn invalid_values_return_err() {
820        let s = store_with(&[
821            ("Channel-State", "BOGUS"),
822            ("Channel-State-Number", "999"),
823            ("Channel-Call-State", "BOGUS"),
824            ("Answer-State", "bogus"),
825            ("Call-Direction", "bogus"),
826            ("Presence-Call-Direction", "bogus"),
827            ("priority", "BOGUS"),
828            ("Hangup-Cause", "BOGUS"),
829        ]);
830        assert!(s
831            .channel_state()
832            .is_err());
833        assert!(s
834            .channel_state_number()
835            .is_err());
836        assert!(s
837            .call_state()
838            .is_err());
839        assert!(s
840            .answer_state()
841            .is_err());
842        assert!(s
843            .call_direction()
844            .is_err());
845        assert!(s
846            .presence_call_direction()
847            .is_err());
848        assert!(s
849            .priority()
850            .is_err());
851        assert!(s
852            .hangup_cause()
853            .is_err());
854    }
855}