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