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