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    channel_driver, AnswerState, CallDirection, CallState, ChannelState, ChannelTimetable,
10    HangupCause, ParseAnswerStateError, ParseCallDirectionError, ParseCallStateError,
11    ParseChannelStateError, 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::{
22    LoopbackLeg, LoopbackResignation, LoopbackVariable, ParseLoopbackLegError, VariableName,
23};
24use sip_header::SipHeaderLookup;
25use std::str::FromStr;
26
27/// Wire key of a channel variable: the [`VARIABLE_PREFIX`](crate::VARIABLE_PREFIX)
28/// ahead of its bare name.
29pub fn variable_key(name: &str) -> String {
30    format!("{}{name}", crate::VARIABLE_PREFIX)
31}
32
33/// Parse an optional wire header value into a typed result.
34///
35/// Collapses the common `match self.header(...) { Some(s) => Ok(Some(s.parse()?)),
36/// None => Ok(None) }` pattern used by many `HeaderLookup` accessors.
37#[inline]
38fn parse_opt<T: FromStr>(raw: Option<&str>) -> Result<Option<T>, T::Err> {
39    raw.map(str::parse)
40        .transpose()
41}
42
43/// Trait for looking up ESL headers and channel variables from any key-value store.
44///
45/// Implementors provide two methods -- `header_str(&str)` and `variable_str(&str)` --
46/// and get all typed accessors (`channel_state()`, `call_direction()`, `timetable()`,
47/// etc.) as default implementations.
48///
49/// `HeaderLookup: SipHeaderLookup` — every `HeaderLookup` implementor must also
50/// implement [`sip_header::SipHeaderLookup`] so callers can reach the typed RFC
51/// SIP parsers (`call_info()`, `history_info()`, `geolocation()`) directly from
52/// the same value. For stores that treat ESL headers and SIP headers as the
53/// same flat key-value namespace, a one-line delegation from `sip_header_str`
54/// to `header_str` is the intended impl — see [`EslEvent`](crate::EslEvent)
55/// for the pattern. Stores with FreeSWITCH-specific transport encoding (ARRAY,
56/// `[bracket]` wrapping) should use [`EslHeaders`](crate::EslHeaders), which
57/// overrides the `SipHeaderLookup` parsers to strip those quirks.
58///
59/// This trait must be in scope to call its methods on `EslEvent` -- including
60/// `unique_id()`, `hangup_cause()`, and `channel_state()`. Import it directly
61/// or via the prelude:
62///
63/// ```ignore
64/// use freeswitch_esl_tokio::prelude::*;
65/// // or: use freeswitch_esl_tokio::HeaderLookup;
66/// // or: use freeswitch_types::HeaderLookup;
67/// ```
68///
69/// # Example
70///
71/// ```
72/// use std::collections::HashMap;
73/// use freeswitch_types::{HeaderLookup, EventHeader, ChannelVariable, VARIABLE_PREFIX};
74/// use freeswitch_types::sip_header::SipHeaderLookup;
75///
76/// struct MyStore(HashMap<String, String>);
77///
78/// // HeaderLookup has SipHeaderLookup as a supertrait. For stores that
79/// // treat ESL and SIP headers as one flat namespace, delegate
80/// // sip_header_str to the same lookup.
81/// impl SipHeaderLookup for MyStore {
82///     fn sip_header_str(&self, name: &str) -> Option<&str> {
83///         self.0.get(name).map(|s| s.as_str())
84///     }
85/// }
86///
87/// impl HeaderLookup for MyStore {
88///     fn header_str(&self, name: &str) -> Option<&str> {
89///         self.0.get(name).map(|s| s.as_str())
90///     }
91///     fn variable_str(&self, name: &str) -> Option<&str> {
92///         self.0.get(&format!("{VARIABLE_PREFIX}{name}")).map(|s| s.as_str())
93///     }
94/// }
95///
96/// let mut map = HashMap::new();
97/// map.insert("Channel-State".into(), "CS_EXECUTE".into());
98/// map.insert("variable_read_codec".into(), "PCMU".into());
99/// let store = MyStore(map);
100///
101/// // Typed accessor from the trait (returns Result<Option<T>, E>):
102/// assert!(store.channel_state().unwrap().is_some());
103///
104/// // Enum-based lookups:
105/// assert_eq!(store.header(EventHeader::ChannelState), Some("CS_EXECUTE"));
106/// assert_eq!(store.variable(ChannelVariable::ReadCodec), Some("PCMU"));
107/// ```
108pub trait HeaderLookup: SipHeaderLookup {
109    /// Look up a header by its raw wire name (e.g. `"Unique-ID"`).
110    fn header_str(&self, name: &str) -> Option<&str>;
111
112    /// Look up a channel variable by its bare name (e.g. `"sip_call_id"`).
113    ///
114    /// Implementations typically prepend `variable_` and delegate to `header_str`.
115    fn variable_str(&self, name: &str) -> Option<&str>;
116
117    /// Look up a header by its [`EventHeader`] enum variant.
118    fn header(&self, name: EventHeader) -> Option<&str> {
119        self.header_str(name.as_str())
120    }
121
122    /// Look up a channel variable by its typed enum variant.
123    fn variable(&self, name: impl VariableName) -> Option<&str> {
124        self.variable_str(name.as_str())
125    }
126
127    /// First of `names` the store carries, probed in order.
128    ///
129    /// For a field the switch spells more than one way: each spelling is its
130    /// own [`EventHeader`] variant, and this unions them without deciding
131    /// which event carries which.
132    fn first_header<const N: usize>(&self, names: [EventHeader; N]) -> Option<&str> {
133        names
134            .into_iter()
135            .find_map(|name| self.header(name))
136    }
137
138    /// `Unique-ID` header, falling back to `Caller-Unique-ID`.
139    fn unique_id(&self) -> Option<&str> {
140        self.first_header([EventHeader::UniqueId, EventHeader::CallerUniqueId])
141    }
142
143    /// `Job-UUID` header from `bgapi` `BACKGROUND_JOB` events.
144    ///
145    /// The only thing separating your job's result from another ESL client's:
146    /// `BACKGROUND_JOB` is fired globally, so seeing one you did not issue is
147    /// routine rather than exceptional.
148    fn job_uuid(&self) -> Option<&str> {
149        self.header(EventHeader::JobUuid)
150    }
151
152    /// `Channel-Name` header (e.g. `sofia/internal/1000@domain`).
153    fn channel_name(&self) -> Option<&str> {
154        self.header(EventHeader::ChannelName)
155    }
156
157    /// Endpoint module the emitting channel belongs to, from its own
158    /// `Channel-Name`.
159    ///
160    /// Never from `Caller-Channel-Name`, which after a loopback resignation
161    /// names the leg that left. See
162    /// [`channel_driver()`](crate::channel::channel_driver) and
163    /// [`LoopbackChannelName`](crate::variables::LoopbackChannelName).
164    fn channel_driver(&self) -> Option<&str> {
165        self.channel_name()
166            .and_then(channel_driver)
167    }
168
169    /// `loopback_leg` variable, for a channel already known to be a loopback.
170    ///
171    /// Not an answer to "is this a loopback channel": the execute-time
172    /// resignation copies every variable onto the real channel that continues
173    /// the call, so this reads `Some` on a survivor that never was one. That
174    /// question is [`LoopbackChannelName`](crate::variables::LoopbackChannelName)'s.
175    fn loopback_leg(&self) -> Result<Option<LoopbackLeg>, ParseLoopbackLegError> {
176        parse_opt(self.variable(LoopbackVariable::LoopbackLeg))
177    }
178
179    /// `Caller-Caller-ID-Number` header.
180    fn caller_id_number(&self) -> Option<&str> {
181        self.header(EventHeader::CallerCallerIdNumber)
182    }
183
184    /// `Caller-Caller-ID-Name` header.
185    fn caller_id_name(&self) -> Option<&str> {
186        self.header(EventHeader::CallerCallerIdName)
187    }
188
189    /// `Caller-Destination-Number` header.
190    fn destination_number(&self) -> Option<&str> {
191        self.header(EventHeader::CallerDestinationNumber)
192    }
193
194    /// `Caller-Callee-ID-Number` header.
195    fn callee_id_number(&self) -> Option<&str> {
196        self.header(EventHeader::CallerCalleeIdNumber)
197    }
198
199    /// `Caller-Callee-ID-Name` header.
200    fn callee_id_name(&self) -> Option<&str> {
201        self.header(EventHeader::CallerCalleeIdName)
202    }
203
204    /// `Channel-Presence-ID` header (e.g. `1000@example.com`).
205    fn channel_presence_id(&self) -> Option<&str> {
206        self.header(EventHeader::ChannelPresenceId)
207    }
208
209    /// `Presence-Call-Direction` header, parsed into a [`CallDirection`].
210    fn presence_call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
211        parse_opt(self.header(EventHeader::PresenceCallDirection))
212    }
213
214    /// `Event-Date-Timestamp` header (microseconds since epoch).
215    fn event_date_timestamp(&self) -> Option<&str> {
216        self.header(EventHeader::EventDateTimestamp)
217    }
218
219    /// `Event-Sequence` header (sequential event counter).
220    fn event_sequence(&self) -> Option<&str> {
221        self.header(EventHeader::EventSequence)
222    }
223
224    /// `DTMF-Duration` header (digit duration in milliseconds).
225    fn dtmf_duration(&self) -> Option<&str> {
226        self.header(EventHeader::DtmfDuration)
227    }
228
229    /// `DTMF-Source` header (e.g. `rtp`, `inband`).
230    fn dtmf_source(&self) -> Option<&str> {
231        self.header(EventHeader::DtmfSource)
232    }
233
234    /// Parse the `Hangup-Cause` header into a [`HangupCause`].
235    ///
236    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
237    fn hangup_cause(&self) -> Result<Option<HangupCause>, ParseHangupCauseError> {
238        parse_opt(self.header(EventHeader::HangupCause))
239    }
240
241    /// Detect a mod_loopback bowout on a `CHANNEL_HANGUP_COMPLETE`.
242    ///
243    /// `Some` means a loopback leg resigned and the call it carried is still up
244    /// on [`other_uuid()`](LoopbackResignation::other_uuid) -- re-anchor
245    /// tracking there rather than treating the hangup as a teardown. `None` is
246    /// a genuine teardown.
247    ///
248    /// `Some` is conclusive only once the emitting channel is known to be a
249    /// loopback: the marker is copied onto the real channel that continues the
250    /// call, so acting on it unqualified tears down a live one. Qualify with
251    /// [`LoopbackChannelName::parse()`](crate::variables::LoopbackChannelName::parse)
252    /// over [`channel_name()`](Self::channel_name).
253    ///
254    /// Keyed on the presence of `loopback_hangup_cause`, never its value:
255    /// mod_loopback writes a different token depending on which internal path
256    /// resigned, and matching one of them silently mis-disposes every call
257    /// that took the other.
258    fn loopback_resignation(&self) -> Option<LoopbackResignation<'_>> {
259        self.variable(LoopbackVariable::LoopbackHangupCause)
260            .map(|cause| {
261                LoopbackResignation::new(
262                    cause,
263                    self.variable(LoopbackVariable::LoopbackBowoutOtherUuid),
264                )
265            })
266    }
267
268    /// `Event-Subclass` header for `CUSTOM` events (e.g. `sofia::register`).
269    fn event_subclass(&self) -> Option<&str> {
270        self.header(EventHeader::EventSubclass)
271    }
272
273    /// Parse `Event-Subclass` as a typed [`SofiaEventSubclass`].
274    ///
275    /// Returns `Ok(None)` if the header is absent, `Err` if present but not a
276    /// recognized `sofia::*` subclass.
277    fn sofia_event_subclass(
278        &self,
279    ) -> Result<Option<SofiaEventSubclass>, ParseSofiaEventSubclassError> {
280        parse_opt(self.event_subclass())
281    }
282
283    /// `Gateway` header from `sofia::gateway_state` / `sofia::gateway_add` events.
284    fn gateway(&self) -> Option<&str> {
285        self.header(EventHeader::Gateway)
286    }
287
288    /// Sofia profile name, from whichever of the two wire spellings the event
289    /// carries.
290    ///
291    /// Probes [`EventHeader::ProfileName`] (`profile-name`, on the
292    /// registration, gateway and user-state `CUSTOM` subclasses) then
293    /// [`EventHeader::ProfileNameSnake`] (`profile_name`, on mod_sofia's
294    /// SIP-method-mirror core events and on
295    /// [`SofiaEventSubclass::ProfileStart`]).
296    /// The spelling does not follow from the event type, so use
297    /// `header(EventHeader::…)` when a spelling-exact lookup is what you want.
298    fn profile_name(&self) -> Option<&str> {
299        self.first_header([EventHeader::ProfileName, EventHeader::ProfileNameSnake])
300    }
301
302    /// `module_name` header -- the literal `mod_sofia` on the events above.
303    fn module_name(&self) -> Option<&str> {
304        self.header(EventHeader::ModuleName)
305    }
306
307    /// `profile_uri` header -- bind URL of the profile named by
308    /// [`profile_name()`](Self::profile_name).
309    fn profile_uri(&self) -> Option<&str> {
310        self.header(EventHeader::ProfileUri)
311    }
312
313    /// `Phrase` header (SIP reason phrase) from sofia state events.
314    fn phrase(&self) -> Option<&str> {
315        self.header(EventHeader::Phrase)
316    }
317
318    /// `Status` header (SIP response code) from `sofia::gateway_state` and
319    /// `sofia::sip_user_state` events.
320    fn sip_status_code(&self) -> Result<Option<u16>, std::num::ParseIntError> {
321        parse_opt(self.header(EventHeader::Status))
322    }
323
324    /// Parse the `State` header as a [`GatewayRegState`].
325    ///
326    /// Returns `Ok(None)` if absent, `Err` if present but unparseable.
327    fn gateway_reg_state(&self) -> Result<Option<GatewayRegState>, ParseGatewayRegStateError> {
328        parse_opt(self.header(EventHeader::State))
329    }
330
331    /// Parse `Ping-Status` as a [`GatewayPingStatus`].
332    ///
333    /// Use on `sofia::gateway_state` events. For `sofia::sip_user_state`, use
334    /// [`sip_user_ping_status()`](Self::sip_user_ping_status) instead.
335    fn gateway_ping_status(
336        &self,
337    ) -> Result<Option<GatewayPingStatus>, ParseGatewayPingStatusError> {
338        parse_opt(self.header(EventHeader::PingStatus))
339    }
340
341    /// Parse `Ping-Status` as a [`SipUserPingStatus`].
342    ///
343    /// Use on `sofia::sip_user_state` events. For `sofia::gateway_state`, use
344    /// [`gateway_ping_status()`](Self::gateway_ping_status) instead.
345    fn sip_user_ping_status(
346        &self,
347    ) -> Result<Option<SipUserPingStatus>, ParseSipUserPingStatusError> {
348        parse_opt(self.header(EventHeader::PingStatus))
349    }
350
351    /// `pl_data` header -- SIP NOTIFY body content from `NOTIFY_IN` events.
352    ///
353    /// Contains the JSON payload (already percent-decoded by the ESL parser).
354    /// For NG9-1-1 events this is the inner object without the wrapper key
355    /// (FreeSWITCH strips it).
356    fn pl_data(&self) -> Option<&str> {
357        self.header(EventHeader::PlData)
358    }
359
360    /// `event` header -- SIP event package name from `NOTIFY_IN` events.
361    ///
362    /// Examples: `emergency-AbandonedCall`, `emergency-ServiceState`.
363    fn sip_event(&self) -> Option<&str> {
364        self.header(EventHeader::SipEvent)
365    }
366
367    /// `gateway_name` header -- gateway that received a SIP NOTIFY.
368    fn gateway_name(&self) -> Option<&str> {
369        self.header(EventHeader::GatewayName)
370    }
371
372    /// Parse the `Channel-State` header into a [`ChannelState`].
373    ///
374    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
375    fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
376        parse_opt(self.header(EventHeader::ChannelState))
377    }
378
379    /// Parse the `Channel-State-Number` header into a [`ChannelState`].
380    ///
381    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
382    ///
383    /// This reports the channel's `state`, which leads `Channel-State`
384    /// (`running_state`) during teardown, so it is not the end-of-life signal --
385    /// see [`is_terminal_channel_state`](Self::is_terminal_channel_state).
386    fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
387        let Some(raw) = self.header(EventHeader::ChannelStateNumber) else {
388            return Ok(None);
389        };
390        let rejected = || ParseChannelStateError(raw.to_string());
391        parse_opt::<u8>(Some(raw))
392            .map_err(|_| rejected())?
393            .and_then(ChannelState::from_number)
394            .ok_or_else(rejected)
395            .map(Some)
396    }
397
398    /// Whether this event is the one that ends the channel: a `CHANNEL_STATE`
399    /// carrying `Channel-State: CS_DESTROY`.
400    ///
401    /// `CHANNEL_DESTROY` is not that event -- its `Channel-State` still reads
402    /// `CS_REPORTING`. Never derive end-of-life from
403    /// [`channel_state_number()`](Self::channel_state_number), which already
404    /// reads `CS_DESTROY` on `CHANNEL_DESTROY` and so fires twice.
405    ///
406    /// `Err` when `Channel-State` is present but unparseable; `Ok(false)` when
407    /// it is absent.
408    fn is_terminal_channel_state(&self) -> Result<bool, ParseChannelStateError> {
409        Ok(self
410            .channel_state()?
411            .is_some_and(|state| state.is_terminal()))
412    }
413
414    /// Parse the `Channel-Call-State` header into a [`CallState`].
415    ///
416    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
417    fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
418        parse_opt(self.header(EventHeader::ChannelCallState))
419    }
420
421    /// Parse the `Answer-State` header into an [`AnswerState`].
422    ///
423    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
424    fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
425        parse_opt(self.header(EventHeader::AnswerState))
426    }
427
428    /// Parse the `Call-Direction` header into a [`CallDirection`].
429    ///
430    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
431    fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
432        parse_opt(self.header(EventHeader::CallDirection))
433    }
434
435    /// Parse the `priority` header value.
436    ///
437    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
438    #[cfg(feature = "esl")]
439    fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
440        parse_opt(self.header(EventHeader::Priority))
441    }
442
443    /// Extract timetable from timestamp headers with the given prefix.
444    ///
445    /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
446    /// Returns `Err` if a header is present but contains an invalid value.
447    fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
448        ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
449    }
450
451    /// Caller-leg channel timetable (`Caller-*-Time` headers).
452    fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
453        self.timetable("Caller")
454    }
455
456    /// Other-leg channel timetable (`Other-Leg-*-Time` headers).
457    fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
458        self.timetable("Other-Leg")
459    }
460}
461
462impl HeaderLookup for std::collections::HashMap<String, String> {
463    fn header_str(&self, name: &str) -> Option<&str> {
464        self.get(name)
465            .map(|s| s.as_str())
466    }
467
468    fn variable_str(&self, name: &str) -> Option<&str> {
469        self.get(&variable_key(name))
470            .map(|s| s.as_str())
471    }
472}
473
474// No blanket impl on `indexmap::IndexMap<String, String>`: both traits are
475// external, so the orphan rules forbid the pair. Wrap in `EslHeaders`.
476
477/// The union a caller reaches when `?`-ing several typed [`HeaderLookup`]
478/// accessors into one error type.
479///
480/// Each accessor returns its own parser's error; a function calling more than
481/// one of them has nothing to name in its return type without this. `Display`
482/// and [`source`](std::error::Error::source) both defer to the inner error, so
483/// nothing is lost by going through it.
484#[derive(Debug, Clone, PartialEq, Eq)]
485#[non_exhaustive]
486pub enum ParseHeaderError {
487    /// From [`channel_state`](HeaderLookup::channel_state) and its siblings.
488    ChannelState(ParseChannelStateError),
489    /// From [`call_state`](HeaderLookup::call_state).
490    CallState(ParseCallStateError),
491    /// From [`answer_state`](HeaderLookup::answer_state).
492    AnswerState(ParseAnswerStateError),
493    /// From [`call_direction`](HeaderLookup::call_direction).
494    CallDirection(ParseCallDirectionError),
495    /// From [`hangup_cause`](HeaderLookup::hangup_cause).
496    HangupCause(ParseHangupCauseError),
497    /// From [`gateway_reg_state`](HeaderLookup::gateway_reg_state).
498    GatewayRegState(ParseGatewayRegStateError),
499    /// From [`loopback_leg`](HeaderLookup::loopback_leg).
500    LoopbackLeg(ParseLoopbackLegError),
501    /// From [`priority`](HeaderLookup::priority).
502    #[cfg(feature = "esl")]
503    Priority(ParsePriorityError),
504    /// From [`caller_timetable`](HeaderLookup::caller_timetable) and
505    /// [`other_leg_timetable`](HeaderLookup::other_leg_timetable).
506    Timetable(ParseTimetableError),
507    /// From [`sip_status_code`](HeaderLookup::sip_status_code), whose header
508    /// parses as a plain integer.
509    ///
510    /// Constructed by name, never through `?`: `ParseIntError` belongs to no
511    /// header, and a `From` for it would relabel every unrelated integer parse
512    /// in the caller as this one.
513    SipStatusCode(std::num::ParseIntError),
514}
515
516/// `From` for each variant that owns its error type outright, plus the two
517/// trait impls over every variant.
518///
519/// A variant listed under `borrowed` carries an error type this crate does not
520/// own and cannot claim: it gets the trait arms and no conversion.
521macro_rules! parse_header_error_impls {
522    (
523        owned { $($(#[$attr:meta])* $Variant:ident($Error:ty)),+ $(,)? }
524        borrowed { $($(#[$battr:meta])* $BVariant:ident),+ $(,)? }
525    ) => {
526        $(
527            $(#[$attr])*
528            impl From<$Error> for ParseHeaderError {
529                fn from(e: $Error) -> Self {
530                    Self::$Variant(e)
531                }
532            }
533        )+
534
535        impl ::std::fmt::Display for ParseHeaderError {
536            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
537                match self {
538                    $($(#[$attr])* Self::$Variant(e) => e.fmt(f),)+
539                    $($(#[$battr])* Self::$BVariant(e) => e.fmt(f),)+
540                }
541            }
542        }
543
544        impl ::std::error::Error for ParseHeaderError {
545            fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
546                match self {
547                    $($(#[$attr])* Self::$Variant(e) => Some(e),)+
548                    $($(#[$battr])* Self::$BVariant(e) => Some(e),)+
549                }
550            }
551        }
552    };
553}
554
555parse_header_error_impls! {
556    owned {
557        ChannelState(ParseChannelStateError),
558        CallState(ParseCallStateError),
559        AnswerState(ParseAnswerStateError),
560        CallDirection(ParseCallDirectionError),
561        HangupCause(ParseHangupCauseError),
562        GatewayRegState(ParseGatewayRegStateError),
563        LoopbackLeg(ParseLoopbackLegError),
564        #[cfg(feature = "esl")]
565        Priority(ParsePriorityError),
566        Timetable(ParseTimetableError),
567    }
568    borrowed {
569        SipStatusCode,
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::variables::{
577        ChannelVariable, LoopbackChannelName, LoopbackHangupCause, LoopbackLeg,
578    };
579    use std::collections::HashMap;
580    use std::error::Error;
581
582    #[test]
583    fn every_accessor_error_converts_into_the_union() {
584        let cases: Vec<(ParseHeaderError, &str)> = vec![
585            (
586                ParseChannelStateError("x".into()).into(),
587                "ChannelState(ParseChannelStateError",
588            ),
589            (
590                ParseCallStateError("x".into()).into(),
591                "CallState(ParseCallStateError",
592            ),
593            (
594                ParseAnswerStateError("x".into()).into(),
595                "AnswerState(ParseAnswerStateError",
596            ),
597            (
598                ParseCallDirectionError("x".into()).into(),
599                "CallDirection(ParseCallDirectionError",
600            ),
601            (
602                ParseHangupCauseError("x".into()).into(),
603                "HangupCause(ParseHangupCauseError",
604            ),
605            (
606                ParseGatewayRegStateError("x".into()).into(),
607                "GatewayRegState(ParseGatewayRegStateError",
608            ),
609            (
610                ParseLoopbackLegError("x".into()).into(),
611                "LoopbackLeg(ParseLoopbackLegError",
612            ),
613            (
614                ParseTimetableError::new("Caller-Channel-Created-Time", "x").into(),
615                "Timetable(ParseTimetableError",
616            ),
617            (
618                ParseHeaderError::SipStatusCode(
619                    "x".parse::<u16>()
620                        .expect_err("non-numeric must fail"),
621                ),
622                "SipStatusCode(ParseIntError",
623            ),
624        ];
625        for (err, expected) in cases {
626            assert!(
627                format!("{err:?}").starts_with(expected),
628                "{err:?} should be {expected}"
629            );
630            assert!(
631                err.source()
632                    .is_some(),
633                "{err:?} must expose its inner error as source"
634            );
635        }
636    }
637
638    #[cfg(feature = "esl")]
639    #[test]
640    fn priority_error_converts_into_the_union() {
641        let err: ParseHeaderError = ParsePriorityError("x".into()).into();
642        assert!(matches!(err, ParseHeaderError::Priority(_)));
643        assert!(err
644            .source()
645            .is_some());
646    }
647
648    // Display delegates: the union adds no prefix of its own, so a caller
649    // printing `{e}` sees exactly what the typed parser said.
650    #[test]
651    fn display_delegates_to_the_inner_error() {
652        let inner = ParseChannelStateError("CS_NONSENSE".into());
653        let expected = inner.to_string();
654        let err: ParseHeaderError = inner.into();
655        assert_eq!(err.to_string(), expected);
656    }
657
658    struct TestStore(HashMap<String, String>);
659
660    impl SipHeaderLookup for TestStore {
661        fn sip_header_str(&self, name: &str) -> Option<&str> {
662            self.0
663                .get(name)
664                .map(|s| s.as_str())
665        }
666    }
667
668    impl HeaderLookup for TestStore {
669        fn header_str(&self, name: &str) -> Option<&str> {
670            self.0
671                .get(name)
672                .map(|s| s.as_str())
673        }
674        fn variable_str(&self, name: &str) -> Option<&str> {
675            self.0
676                .get(&variable_key(name))
677                .map(|s| s.as_str())
678        }
679    }
680
681    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
682        let map: HashMap<String, String> = pairs
683            .iter()
684            .map(|(k, v)| (k.to_string(), v.to_string()))
685            .collect();
686        TestStore(map)
687    }
688
689    #[test]
690    fn header_str_direct() {
691        let s = store_with(&[("Unique-ID", "abc-123")]);
692        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
693        assert_eq!(s.header_str("Missing"), None);
694    }
695
696    #[test]
697    fn header_by_enum() {
698        let s = store_with(&[("Unique-ID", "abc-123")]);
699        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
700    }
701
702    #[test]
703    fn variable_str_direct() {
704        let s = store_with(&[("variable_read_codec", "PCMU")]);
705        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
706        assert_eq!(s.variable_str("missing"), None);
707    }
708
709    #[test]
710    fn variable_by_enum() {
711        let s = store_with(&[("variable_read_codec", "PCMU")]);
712        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
713    }
714
715    #[test]
716    fn unique_id_primary() {
717        let s = store_with(&[("Unique-ID", "uuid-1")]);
718        assert_eq!(s.unique_id(), Some("uuid-1"));
719    }
720
721    #[test]
722    fn unique_id_fallback() {
723        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
724        assert_eq!(s.unique_id(), Some("uuid-2"));
725    }
726
727    #[test]
728    fn unique_id_none() {
729        let s = store_with(&[]);
730        assert_eq!(s.unique_id(), None);
731    }
732
733    #[test]
734    fn job_uuid() {
735        let s = store_with(&[("Job-UUID", "job-1")]);
736        assert_eq!(s.job_uuid(), Some("job-1"));
737    }
738
739    #[test]
740    fn channel_name() {
741        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
742        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
743    }
744
745    #[test]
746    fn caller_id_number_and_name() {
747        let s = store_with(&[
748            ("Caller-Caller-ID-Number", "1000"),
749            ("Caller-Caller-ID-Name", "Alice"),
750        ]);
751        assert_eq!(s.caller_id_number(), Some("1000"));
752        assert_eq!(s.caller_id_name(), Some("Alice"));
753    }
754
755    #[test]
756    fn hangup_cause_typed() {
757        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
758        assert_eq!(
759            s.hangup_cause()
760                .unwrap(),
761            Some(crate::channel::HangupCause::NormalClearing)
762        );
763    }
764
765    #[test]
766    fn hangup_cause_invalid_is_error() {
767        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
768        assert!(s
769            .hangup_cause()
770            .is_err());
771    }
772
773    #[test]
774    fn destination_number() {
775        let s = store_with(&[("Caller-Destination-Number", "1000")]);
776        assert_eq!(s.destination_number(), Some("1000"));
777    }
778
779    #[test]
780    fn callee_id() {
781        let s = store_with(&[
782            ("Caller-Callee-ID-Number", "2000"),
783            ("Caller-Callee-ID-Name", "Bob"),
784        ]);
785        assert_eq!(s.callee_id_number(), Some("2000"));
786        assert_eq!(s.callee_id_name(), Some("Bob"));
787    }
788
789    #[test]
790    fn event_subclass() {
791        let s = store_with(&[("Event-Subclass", "sofia::register")]);
792        assert_eq!(s.event_subclass(), Some("sofia::register"));
793    }
794
795    #[test]
796    fn sofia_event_subclass_typed() {
797        let s = store_with(&[("Event-Subclass", "sofia::gateway_state")]);
798        assert_eq!(
799            s.sofia_event_subclass()
800                .unwrap(),
801            Some(crate::sofia::SofiaEventSubclass::GatewayState)
802        );
803    }
804
805    #[test]
806    fn sofia_event_subclass_absent() {
807        let s = store_with(&[]);
808        assert_eq!(
809            s.sofia_event_subclass()
810                .unwrap(),
811            None
812        );
813    }
814
815    #[test]
816    fn sofia_event_subclass_non_sofia_is_error() {
817        let s = store_with(&[("Event-Subclass", "conference::maintenance")]);
818        assert!(s
819            .sofia_event_subclass()
820            .is_err());
821    }
822
823    #[test]
824    fn gateway_reg_state_typed() {
825        let s = store_with(&[("State", "REGED")]);
826        assert_eq!(
827            s.gateway_reg_state()
828                .unwrap(),
829            Some(crate::sofia::GatewayRegState::Reged)
830        );
831    }
832
833    #[test]
834    fn gateway_reg_state_invalid_is_error() {
835        let s = store_with(&[("State", "BOGUS")]);
836        assert!(s
837            .gateway_reg_state()
838            .is_err());
839    }
840
841    #[test]
842    fn gateway_ping_status_typed() {
843        let s = store_with(&[("Ping-Status", "UP")]);
844        assert_eq!(
845            s.gateway_ping_status()
846                .unwrap(),
847            Some(crate::sofia::GatewayPingStatus::Up)
848        );
849    }
850
851    #[test]
852    fn sip_user_ping_status_typed() {
853        let s = store_with(&[("Ping-Status", "REACHABLE")]);
854        assert_eq!(
855            s.sip_user_ping_status()
856                .unwrap(),
857            Some(crate::sofia::SipUserPingStatus::Reachable)
858        );
859    }
860
861    #[test]
862    fn gateway_accessor() {
863        let s = store_with(&[("Gateway", "my-gateway")]);
864        assert_eq!(s.gateway(), Some("my-gateway"));
865    }
866
867    #[test]
868    fn profile_name_accessor() {
869        let s = store_with(&[("profile-name", "internal")]);
870        assert_eq!(s.profile_name(), Some("internal"));
871    }
872
873    #[test]
874    fn profile_name_spellings_resolve_distinct_variants() {
875        let hyphen = store_with(&[("profile-name", "internal")]);
876        assert_eq!(hyphen.header(EventHeader::ProfileName), Some("internal"));
877        assert_eq!(hyphen.header(EventHeader::ProfileNameSnake), None);
878        assert_eq!(hyphen.profile_name(), Some("internal"));
879
880        let underscore = store_with(&[("profile_name", "external")]);
881        assert_eq!(underscore.header(EventHeader::ProfileName), None);
882        assert_eq!(
883            underscore.header(EventHeader::ProfileNameSnake),
884            Some("external")
885        );
886        assert_eq!(underscore.profile_name(), Some("external"));
887    }
888
889    #[test]
890    fn profile_name_probes_hyphen_before_underscore() {
891        let s = store_with(&[("profile-name", "internal"), ("profile_name", "external")]);
892        assert_eq!(s.profile_name(), Some("internal"));
893    }
894
895    #[test]
896    fn module_name_accessor() {
897        let s = store_with(&[("module_name", "mod_sofia")]);
898        assert_eq!(s.module_name(), Some("mod_sofia"));
899    }
900
901    #[test]
902    fn profile_uri_accessor() {
903        let s = store_with(&[("profile_uri", "sip:mod_sofia@192.0.2.1:5060")]);
904        assert_eq!(s.profile_uri(), Some("sip:mod_sofia@192.0.2.1:5060"));
905    }
906
907    #[test]
908    fn phrase_accessor() {
909        let s = store_with(&[("Phrase", "OK")]);
910        assert_eq!(s.phrase(), Some("OK"));
911    }
912
913    #[test]
914    fn channel_state_typed() {
915        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
916        assert_eq!(
917            s.channel_state()
918                .unwrap(),
919            Some(ChannelState::CsExecute)
920        );
921    }
922
923    #[test]
924    fn channel_state_number_typed() {
925        let s = store_with(&[("Channel-State-Number", "4")]);
926        assert_eq!(
927            s.channel_state_number()
928                .unwrap(),
929            Some(ChannelState::CsExecute)
930        );
931    }
932
933    #[test]
934    fn terminal_state_is_the_final_channel_state_event() {
935        let s = store_with(&[("Channel-State", "CS_DESTROY")]);
936        assert_eq!(s.is_terminal_channel_state(), Ok(true));
937    }
938
939    #[test]
940    fn channel_destroy_shape_is_not_terminal() {
941        let s = store_with(&[
942            ("Channel-State", "CS_REPORTING"),
943            ("Channel-State-Number", "12"),
944        ]);
945        assert_eq!(s.is_terminal_channel_state(), Ok(false));
946    }
947
948    #[test]
949    fn hangup_state_is_not_terminal() {
950        let s = store_with(&[("Channel-State", "CS_HANGUP")]);
951        assert_eq!(s.is_terminal_channel_state(), Ok(false));
952    }
953
954    #[test]
955    fn unparseable_channel_state_is_an_error() {
956        let s = store_with(&[("Channel-State", "CS_BOGUS")]);
957        assert!(s
958            .is_terminal_channel_state()
959            .is_err());
960    }
961
962    #[test]
963    fn missing_channel_state_is_not_terminal() {
964        let s = store_with(&[("Event-Name", "HEARTBEAT")]);
965        assert_eq!(s.is_terminal_channel_state(), Ok(false));
966    }
967
968    #[test]
969    fn call_state_typed() {
970        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
971        assert_eq!(
972            s.call_state()
973                .unwrap(),
974            Some(CallState::Active)
975        );
976    }
977
978    #[test]
979    fn answer_state_typed() {
980        let s = store_with(&[("Answer-State", "answered")]);
981        assert_eq!(
982            s.answer_state()
983                .unwrap(),
984            Some(AnswerState::Answered)
985        );
986    }
987
988    #[test]
989    fn call_direction_typed() {
990        let s = store_with(&[("Call-Direction", "inbound")]);
991        assert_eq!(
992            s.call_direction()
993                .unwrap(),
994            Some(CallDirection::Inbound)
995        );
996    }
997
998    #[cfg(feature = "esl")]
999    #[test]
1000    fn priority_typed() {
1001        let s = store_with(&[("priority", "HIGH")]);
1002        assert_eq!(
1003            s.priority()
1004                .unwrap(),
1005            Some(EslEventPriority::High)
1006        );
1007        assert_eq!(
1008            store_with(&[])
1009                .priority()
1010                .unwrap(),
1011            None
1012        );
1013        assert!(store_with(&[("priority", "BOGUS")])
1014            .priority()
1015            .is_err());
1016    }
1017
1018    #[test]
1019    fn timetable_extraction() {
1020        let s = store_with(&[
1021            ("Caller-Channel-Created-Time", "1700000001000000"),
1022            ("Caller-Channel-Answered-Time", "1700000005000000"),
1023        ]);
1024        let tt = s
1025            .caller_timetable()
1026            .unwrap()
1027            .expect("should have timetable");
1028        assert_eq!(tt.created, Some(1700000001000000));
1029        assert_eq!(tt.answered, Some(1700000005000000));
1030        assert_eq!(tt.hungup, None);
1031    }
1032
1033    #[test]
1034    fn timetable_other_leg() {
1035        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
1036        let tt = s
1037            .other_leg_timetable()
1038            .unwrap()
1039            .expect("should have timetable");
1040        assert_eq!(tt.created, Some(1700000001000000));
1041    }
1042
1043    #[test]
1044    fn timetable_none_when_absent() {
1045        let s = store_with(&[]);
1046        assert_eq!(
1047            s.caller_timetable()
1048                .unwrap(),
1049            None
1050        );
1051    }
1052
1053    #[test]
1054    fn timetable_invalid_is_error() {
1055        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
1056        let err = s
1057            .caller_timetable()
1058            .unwrap_err();
1059        assert_eq!(err.header, "Caller-Channel-Created-Time");
1060    }
1061
1062    // --- loopback resignation ---
1063
1064    #[test]
1065    fn loopback_resignation_absent_on_a_real_teardown() {
1066        let s = store_with(&[("variable_hangup_cause", "NORMAL_CLEARING")]);
1067        assert!(s
1068            .loopback_resignation()
1069            .is_none());
1070    }
1071
1072    #[test]
1073    fn loopback_resignation_on_execute_masquerade_path() {
1074        let s = store_with(&[
1075            ("variable_loopback_hangup_cause", "bowout"),
1076            ("variable_loopback_bowout_other_uuid", "survivor-uuid"),
1077        ]);
1078        let r = s
1079            .loopback_resignation()
1080            .expect("marker present");
1081        assert_eq!(r.cause(), Ok(LoopbackHangupCause::Bowout));
1082        assert_eq!(r.cause_raw(), "bowout");
1083        assert_eq!(r.other_uuid(), Some("survivor-uuid"));
1084    }
1085
1086    #[test]
1087    fn loopback_resignation_on_frame_count_path() {
1088        let s = store_with(&[
1089            ("variable_loopback_hangup_cause", "bridge"),
1090            ("variable_loopback_bowout_other_uuid", "survivor-uuid"),
1091        ]);
1092        let r = s
1093            .loopback_resignation()
1094            .expect("marker present");
1095        assert_eq!(r.cause(), Ok(LoopbackHangupCause::Bridge));
1096        assert_eq!(r.other_uuid(), Some("survivor-uuid"));
1097    }
1098
1099    // The whole point of the shape: a path mod_loopback grows later still
1100    // reports a resignation, so the surviving leg is never mis-disposed.
1101    #[test]
1102    fn loopback_resignation_survives_an_unknown_token() {
1103        let s = store_with(&[
1104            ("variable_loopback_hangup_cause", "some_future_path"),
1105            ("variable_loopback_bowout_other_uuid", "survivor-uuid"),
1106        ]);
1107        let r = s
1108            .loopback_resignation()
1109            .expect("presence is the signal, not the value");
1110        assert_eq!(r.other_uuid(), Some("survivor-uuid"));
1111        assert_eq!(r.cause_raw(), "some_future_path");
1112        assert!(r
1113            .cause()
1114            .is_err());
1115    }
1116
1117    #[test]
1118    fn loopback_resignation_without_a_surviving_uuid() {
1119        let s = store_with(&[("variable_loopback_hangup_cause", "bridge")]);
1120        let r = s
1121            .loopback_resignation()
1122            .expect("marker present");
1123        assert_eq!(r.other_uuid(), None);
1124    }
1125
1126    // --- channel kind ---
1127
1128    #[test]
1129    fn the_resigning_leg_names_itself() {
1130        let leg = store_with(&[
1131            ("Channel-Name", "loopback/9199-a"),
1132            ("variable_loopback_hangup_cause", "bowout"),
1133            ("variable_loopback_leg", "A"),
1134        ]);
1135        assert_eq!(leg.channel_driver(), Some("loopback"));
1136        let name = leg
1137            .channel_name()
1138            .and_then(LoopbackChannelName::parse)
1139            .expect("a loopback name");
1140        assert_eq!(name.extension(), "9199");
1141        assert_eq!(name.leg(), LoopbackLeg::A);
1142        assert_eq!(
1143            leg.loopback_leg()
1144                .unwrap(),
1145            Some(LoopbackLeg::A)
1146        );
1147    }
1148
1149    // The masquerade copies every variable and the caller profile onto the
1150    // survivor, so only its own Channel-Name still answers for its driver.
1151    #[test]
1152    fn a_survivor_that_inherited_the_marker_is_not_a_loopback() {
1153        let survivor = store_with(&[
1154            ("Channel-Name", "sofia/internal/1000@example.com"),
1155            ("Caller-Channel-Name", "loopback/9199-a"),
1156            ("variable_loopback_hangup_cause", "bowout"),
1157            ("variable_is_loopback", "true"),
1158            ("variable_loopback_leg", "A"),
1159        ]);
1160        assert!(survivor
1161            .loopback_resignation()
1162            .is_some());
1163        assert_eq!(
1164            survivor
1165                .loopback_leg()
1166                .unwrap(),
1167            Some(LoopbackLeg::A)
1168        );
1169
1170        assert_eq!(survivor.channel_driver(), Some("sofia"));
1171        assert_eq!(
1172            survivor
1173                .channel_name()
1174                .and_then(LoopbackChannelName::parse),
1175            None
1176        );
1177    }
1178
1179    #[test]
1180    fn channel_kind_accessors_absent() {
1181        let s = store_with(&[]);
1182        assert_eq!(s.channel_driver(), None);
1183        assert_eq!(
1184            s.loopback_leg()
1185                .unwrap(),
1186            None
1187        );
1188    }
1189
1190    #[test]
1191    fn loopback_leg_invalid_is_error() {
1192        let s = store_with(&[("variable_loopback_leg", "a")]);
1193        assert!(s
1194            .loopback_leg()
1195            .is_err());
1196    }
1197
1198    /// Accessor under test, named for the assertion message.
1199    type RawAccessor = (&'static str, fn(&TestStore) -> Option<&str>);
1200    /// An accessor reported as two booleans: parsed to nothing, and refused.
1201    type TypedAccessor = (&'static str, fn(&TestStore) -> bool, fn(&TestStore) -> bool);
1202
1203    const RAW_ACCESSORS: &[RawAccessor] = &[
1204        ("channel_name", |s| s.channel_name()),
1205        ("caller_id_number", |s| s.caller_id_number()),
1206        ("caller_id_name", |s| s.caller_id_name()),
1207        ("destination_number", |s| s.destination_number()),
1208        ("callee_id_number", |s| s.callee_id_number()),
1209        ("callee_id_name", |s| s.callee_id_name()),
1210        ("event_subclass", |s| s.event_subclass()),
1211        ("job_uuid", |s| s.job_uuid()),
1212        ("pl_data", |s| s.pl_data()),
1213        ("sip_event", |s| s.sip_event()),
1214        ("gateway_name", |s| s.gateway_name()),
1215        ("channel_presence_id", |s| s.channel_presence_id()),
1216        ("event_date_timestamp", |s| s.event_date_timestamp()),
1217        ("event_sequence", |s| s.event_sequence()),
1218        ("dtmf_duration", |s| s.dtmf_duration()),
1219        ("dtmf_source", |s| s.dtmf_source()),
1220    ];
1221
1222    const TYPED_ACCESSORS: &[TypedAccessor] = &[
1223        (
1224            "channel_state",
1225            |s| {
1226                s.channel_state()
1227                    .unwrap()
1228                    .is_none()
1229            },
1230            |s| {
1231                s.channel_state()
1232                    .is_err()
1233            },
1234        ),
1235        (
1236            "channel_state_number",
1237            |s| {
1238                s.channel_state_number()
1239                    .unwrap()
1240                    .is_none()
1241            },
1242            |s| {
1243                s.channel_state_number()
1244                    .is_err()
1245            },
1246        ),
1247        (
1248            "call_state",
1249            |s| {
1250                s.call_state()
1251                    .unwrap()
1252                    .is_none()
1253            },
1254            |s| {
1255                s.call_state()
1256                    .is_err()
1257            },
1258        ),
1259        (
1260            "answer_state",
1261            |s| {
1262                s.answer_state()
1263                    .unwrap()
1264                    .is_none()
1265            },
1266            |s| {
1267                s.answer_state()
1268                    .is_err()
1269            },
1270        ),
1271        (
1272            "call_direction",
1273            |s| {
1274                s.call_direction()
1275                    .unwrap()
1276                    .is_none()
1277            },
1278            |s| {
1279                s.call_direction()
1280                    .is_err()
1281            },
1282        ),
1283        (
1284            "presence_call_direction",
1285            |s| {
1286                s.presence_call_direction()
1287                    .unwrap()
1288                    .is_none()
1289            },
1290            |s| {
1291                s.presence_call_direction()
1292                    .is_err()
1293            },
1294        ),
1295        (
1296            "hangup_cause",
1297            |s| {
1298                s.hangup_cause()
1299                    .unwrap()
1300                    .is_none()
1301            },
1302            |s| {
1303                s.hangup_cause()
1304                    .is_err()
1305            },
1306        ),
1307    ];
1308
1309    #[test]
1310    fn missing_headers_return_none() {
1311        let s = store_with(&[]);
1312        for (name, accessor) in RAW_ACCESSORS {
1313            assert_eq!(accessor(&s), None, "{name}");
1314        }
1315        for (name, absent_is_none, _) in TYPED_ACCESSORS {
1316            assert!(absent_is_none(&s), "{name}");
1317        }
1318    }
1319
1320    #[test]
1321    fn notify_in_headers() {
1322        let s = store_with(&[
1323            ("pl_data", r#"{"invite":"INVITE ..."}"#),
1324            ("event", "emergency-AbandonedCall"),
1325            ("gateway_name", "ng911-bcf"),
1326        ]);
1327        assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
1328        assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
1329        assert_eq!(s.gateway_name(), Some("ng911-bcf"));
1330    }
1331
1332    #[test]
1333    fn channel_presence_id() {
1334        let s = store_with(&[("Channel-Presence-ID", "1000@example.com")]);
1335        assert_eq!(s.channel_presence_id(), Some("1000@example.com"));
1336    }
1337
1338    #[test]
1339    fn presence_call_direction_typed() {
1340        let s = store_with(&[("Presence-Call-Direction", "outbound")]);
1341        assert_eq!(
1342            s.presence_call_direction()
1343                .unwrap(),
1344            Some(CallDirection::Outbound)
1345        );
1346    }
1347
1348    #[test]
1349    fn event_date_timestamp() {
1350        let s = store_with(&[("Event-Date-Timestamp", "1700000001000000")]);
1351        assert_eq!(s.event_date_timestamp(), Some("1700000001000000"));
1352    }
1353
1354    #[test]
1355    fn event_sequence() {
1356        let s = store_with(&[("Event-Sequence", "12345")]);
1357        assert_eq!(s.event_sequence(), Some("12345"));
1358    }
1359
1360    #[test]
1361    fn dtmf_duration() {
1362        let s = store_with(&[("DTMF-Duration", "2000")]);
1363        assert_eq!(s.dtmf_duration(), Some("2000"));
1364    }
1365
1366    #[test]
1367    fn dtmf_source() {
1368        let s = store_with(&[("DTMF-Source", "rtp")]);
1369        assert_eq!(s.dtmf_source(), Some("rtp"));
1370    }
1371
1372    #[test]
1373    fn invalid_values_return_err() {
1374        let s = store_with(&[
1375            ("Channel-State", "BOGUS"),
1376            ("Channel-State-Number", "999"),
1377            ("Channel-Call-State", "BOGUS"),
1378            ("Answer-State", "bogus"),
1379            ("Call-Direction", "bogus"),
1380            ("Presence-Call-Direction", "bogus"),
1381            ("Hangup-Cause", "BOGUS"),
1382        ]);
1383        for (name, _, unparseable_is_err) in TYPED_ACCESSORS {
1384            assert!(unparseable_is_err(&s), "{name}");
1385        }
1386    }
1387}