Skip to main content

freeswitch_types/
channel.rs

1//! Channel-related data types extracted from ESL event headers.
2
3use std::fmt;
4
5wire_enum! {
6    /// Channel state from `switch_channel_state_t` -- carried in the `Channel-State` header
7    /// as a string (`CS_ROUTING`) and in `Channel-State-Number` as an integer.
8    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9    #[repr(u8)]
10    pub enum ChannelState {
11        CsNew = 0 => "CS_NEW",
12        CsInit = 1 => "CS_INIT",
13        CsRouting = 2 => "CS_ROUTING",
14        CsSoftExecute = 3 => "CS_SOFT_EXECUTE",
15        CsExecute = 4 => "CS_EXECUTE",
16        CsExchangeMedia = 5 => "CS_EXCHANGE_MEDIA",
17        CsPark = 6 => "CS_PARK",
18        CsConsumeMedia = 7 => "CS_CONSUME_MEDIA",
19        CsHibernate = 8 => "CS_HIBERNATE",
20        CsReset = 9 => "CS_RESET",
21        CsHangup = 10 => "CS_HANGUP",
22        CsReporting = 11 => "CS_REPORTING",
23        CsDestroy = 12 => "CS_DESTROY",
24        CsNone = 13 => "CS_NONE",
25    }
26    error ParseChannelStateError("channel state");
27    tests: channel_state_wire_tests;
28}
29
30impl ChannelState {
31    /// Parse from the `Channel-State-Number` integer header value.
32    pub fn from_number(n: u8) -> Option<Self> {
33        match n {
34            0 => Some(Self::CsNew),
35            1 => Some(Self::CsInit),
36            2 => Some(Self::CsRouting),
37            3 => Some(Self::CsSoftExecute),
38            4 => Some(Self::CsExecute),
39            5 => Some(Self::CsExchangeMedia),
40            6 => Some(Self::CsPark),
41            7 => Some(Self::CsConsumeMedia),
42            8 => Some(Self::CsHibernate),
43            9 => Some(Self::CsReset),
44            10 => Some(Self::CsHangup),
45            11 => Some(Self::CsReporting),
46            12 => Some(Self::CsDestroy),
47            13 => Some(Self::CsNone),
48            _ => None,
49        }
50    }
51
52    /// Integer discriminant matching `switch_channel_state_t`.
53    pub fn as_number(&self) -> u8 {
54        *self as u8
55    }
56
57    /// Whether the channel is gone: `CS_DESTROY`, and nothing else.
58    ///
59    /// The session's run loop exits on `CS_DESTROY`; `CS_NONE` is a
60    /// `running_state` sentinel the state machine aborts on if it ever reaches
61    /// it, so it ends nothing despite sorting above `CS_DESTROY`.
62    ///
63    /// `CHANNEL_STATE` events carry no `variable_*` headers, so end-of-life
64    /// detected here cannot be paired with any channel variable from the same
65    /// event. The variable-bearing terminal event is `CHANNEL_HANGUP_COMPLETE`,
66    /// which is also where `variable_hangup_cause` first appears --
67    /// `CHANNEL_HANGUP` carries only the `Hangup-Cause` header.
68    pub fn is_terminal(&self) -> bool {
69        matches!(self, Self::CsDestroy)
70    }
71}
72
73/// Endpoint module a channel name belongs to: the segment before the first `/`
74/// (`sofia`, `loopback`, `null`, ...).
75///
76/// No enum, because any module can register an endpoint. A channel name is also
77/// what the channel calls itself rather than proof of its driver -- see
78/// [`LoopbackChannelName`](crate::variables::LoopbackChannelName) for what that
79/// costs after a loopback resignation.
80pub fn channel_driver(name: &str) -> Option<&str> {
81    let (driver, _) = name.split_once('/')?;
82    if driver.is_empty() {
83        return None;
84    }
85    Some(driver)
86}
87
88wire_enum! {
89    /// Call state from `switch_channel_callstate_t` -- carried in the `Channel-Call-State` header.
90    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
91    pub enum CallState {
92        Down => "DOWN",
93        Dialing => "DIALING",
94        Ringing => "RINGING",
95        Early => "EARLY",
96        Active => "ACTIVE",
97        Held => "HELD",
98        RingWait => "RING_WAIT",
99        Hangup => "HANGUP",
100        Unheld => "UNHELD",
101    }
102    error ParseCallStateError("call state");
103    tests: call_state_wire_tests;
104}
105
106wire_enum! {
107    /// Answer state from the `Answer-State` header. Wire format is lowercase.
108    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109    pub enum AnswerState {
110        Hangup => "hangup",
111        Answered => "answered",
112        Early => "early",
113        Ringing => "ringing",
114    }
115    error ParseAnswerStateError("answer state");
116    tests: answer_state_wire_tests;
117}
118
119wire_enum! {
120    /// Call direction from the `Call-Direction` header. Wire format is lowercase.
121    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122    pub enum CallDirection {
123        Inbound => "inbound",
124        Outbound => "outbound",
125    }
126    error ParseCallDirectionError("call direction");
127    tests: call_direction_wire_tests;
128}
129
130wire_enum! {
131    /// Hangup cause from `switch_cause_t` (Q.850 + FreeSWITCH extensions).
132    ///
133    /// Carried in the `Hangup-Cause` header. Wire format is `SCREAMING_SNAKE_CASE`
134    /// (e.g. `NORMAL_CLEARING`). The numeric value matches the Q.850 cause code
135    /// for standard causes, or a FreeSWITCH-internal range for extensions.
136    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137    #[repr(u16)]
138    pub enum HangupCause {
139        None = 0 => "NONE",
140        UnallocatedNumber = 1 => "UNALLOCATED_NUMBER",
141        NoRouteTransitNet = 2 => "NO_ROUTE_TRANSIT_NET",
142        NoRouteDestination = 3 => "NO_ROUTE_DESTINATION",
143        ChannelUnacceptable = 6 => "CHANNEL_UNACCEPTABLE",
144        CallAwardedDelivered = 7 => "CALL_AWARDED_DELIVERED",
145        NormalClearing = 16 => "NORMAL_CLEARING",
146        UserBusy = 17 => "USER_BUSY",
147        NoUserResponse = 18 => "NO_USER_RESPONSE",
148        NoAnswer = 19 => "NO_ANSWER",
149        SubscriberAbsent = 20 => "SUBSCRIBER_ABSENT",
150        CallRejected = 21 => "CALL_REJECTED",
151        NumberChanged = 22 => "NUMBER_CHANGED",
152        RedirectionToNewDestination = 23 => "REDIRECTION_TO_NEW_DESTINATION",
153        ExchangeRoutingError = 25 => "EXCHANGE_ROUTING_ERROR",
154        DestinationOutOfOrder = 27 => "DESTINATION_OUT_OF_ORDER",
155        InvalidNumberFormat = 28 => "INVALID_NUMBER_FORMAT",
156        FacilityRejected = 29 => "FACILITY_REJECTED",
157        ResponseToStatusEnquiry = 30 => "RESPONSE_TO_STATUS_ENQUIRY",
158        NormalUnspecified = 31 => "NORMAL_UNSPECIFIED",
159        NormalCircuitCongestion = 34 => "NORMAL_CIRCUIT_CONGESTION",
160        NetworkOutOfOrder = 38 => "NETWORK_OUT_OF_ORDER",
161        NormalTemporaryFailure = 41 => "NORMAL_TEMPORARY_FAILURE",
162        SwitchCongestion = 42 => "SWITCH_CONGESTION",
163        AccessInfoDiscarded = 43 => "ACCESS_INFO_DISCARDED",
164        RequestedChanUnavail = 44 => "REQUESTED_CHAN_UNAVAIL",
165        PreEmpted = 45 => "PRE_EMPTED",
166        FacilityNotSubscribed = 50 => "FACILITY_NOT_SUBSCRIBED",
167        OutgoingCallBarred = 52 => "OUTGOING_CALL_BARRED",
168        IncomingCallBarred = 54 => "INCOMING_CALL_BARRED",
169        BearercapabilityNotauth = 57 => "BEARERCAPABILITY_NOTAUTH",
170        BearercapabilityNotavail = 58 => "BEARERCAPABILITY_NOTAVAIL",
171        ServiceUnavailable = 63 => "SERVICE_UNAVAILABLE",
172        BearercapabilityNotimpl = 65 => "BEARERCAPABILITY_NOTIMPL",
173        ChanNotImplemented = 66 => "CHAN_NOT_IMPLEMENTED",
174        FacilityNotImplemented = 69 => "FACILITY_NOT_IMPLEMENTED",
175        ServiceNotImplemented = 79 => "SERVICE_NOT_IMPLEMENTED",
176        InvalidCallReference = 81 => "INVALID_CALL_REFERENCE",
177        IncompatibleDestination = 88 => "INCOMPATIBLE_DESTINATION",
178        InvalidMsgUnspecified = 95 => "INVALID_MSG_UNSPECIFIED",
179        MandatoryIeMissing = 96 => "MANDATORY_IE_MISSING",
180        MessageTypeNonexist = 97 => "MESSAGE_TYPE_NONEXIST",
181        WrongMessage = 98 => "WRONG_MESSAGE",
182        IeNonexist = 99 => "IE_NONEXIST",
183        InvalidIeContents = 100 => "INVALID_IE_CONTENTS",
184        WrongCallState = 101 => "WRONG_CALL_STATE",
185        RecoveryOnTimerExpire = 102 => "RECOVERY_ON_TIMER_EXPIRE",
186        MandatoryIeLengthError = 103 => "MANDATORY_IE_LENGTH_ERROR",
187        ProtocolError = 111 => "PROTOCOL_ERROR",
188        Interworking = 127 => "INTERWORKING",
189        Success = 142 => "SUCCESS",
190        OriginatorCancel = 487 => "ORIGINATOR_CANCEL",
191        Crash = 700 => "CRASH",
192        SystemShutdown = 701 => "SYSTEM_SHUTDOWN",
193        LoseRace = 702 => "LOSE_RACE",
194        ManagerRequest = 703 => "MANAGER_REQUEST",
195        BlindTransfer = 800 => "BLIND_TRANSFER",
196        AttendedTransfer = 801 => "ATTENDED_TRANSFER",
197        AllottedTimeout = 802 => "ALLOTTED_TIMEOUT",
198        UserChallenge = 803 => "USER_CHALLENGE",
199        MediaTimeout = 804 => "MEDIA_TIMEOUT",
200        PickedOff = 805 => "PICKED_OFF",
201        UserNotRegistered = 806 => "USER_NOT_REGISTERED",
202        ProgressTimeout = 807 => "PROGRESS_TIMEOUT",
203        InvalidGateway = 808 => "INVALID_GATEWAY",
204        GatewayDown = 809 => "GATEWAY_DOWN",
205        InvalidUrl = 810 => "INVALID_URL",
206        InvalidProfile = 811 => "INVALID_PROFILE",
207        NoPickup = 812 => "NO_PICKUP",
208        SrtpReadError = 813 => "SRTP_READ_ERROR",
209        Bowout = 814 => "BOWOUT",
210        BusyEverywhere = 815 => "BUSY_EVERYWHERE",
211        Decline = 816 => "DECLINE",
212        DoesNotExistAnywhere = 817 => "DOES_NOT_EXIST_ANYWHERE",
213        NotAcceptable = 818 => "NOT_ACCEPTABLE",
214        Unwanted = 819 => "UNWANTED",
215        NoIdentity = 820 => "NO_IDENTITY",
216        BadIdentityInfo = 821 => "BAD_IDENTITY_INFO",
217        UnsupportedCertificate = 822 => "UNSUPPORTED_CERTIFICATE",
218        InvalidIdentity = 823 => "INVALID_IDENTITY",
219        /// Stale Date (STIR/SHAKEN).
220        StaleDate = 824 => "STALE_DATE",
221        /// Reject all calls.
222        RejectAll = 825 => "REJECT_ALL",
223    }
224    error ParseHangupCauseError("hangup cause");
225    numeric: from_number(u16);
226    tests: hangup_cause_wire_tests;
227}
228
229impl HangupCause {
230    /// Map a SIP response code to the corresponding FreeSWITCH hangup cause.
231    ///
232    /// Uses the same mapping as mod_sofia's `sofia_glue_sip_cause_to_freeswitch()`.
233    /// Returns `None` for codes without an explicit mapping.
234    pub fn from_sip_response(code: u16) -> Option<Self> {
235        match code {
236            200 => Some(Self::NormalClearing),
237            401 | 402 | 403 | 407 | 603 | 608 => Some(Self::CallRejected),
238            607 => Some(Self::Unwanted),
239            404 => Some(Self::UnallocatedNumber),
240            485 | 604 => Some(Self::NoRouteDestination),
241            408 | 504 => Some(Self::RecoveryOnTimerExpire),
242            410 => Some(Self::NumberChanged),
243            413 | 414 | 416 | 420 | 421 | 423 | 505 | 513 => Some(Self::Interworking),
244            480 => Some(Self::NoUserResponse),
245            400 | 481 | 500 | 503 => Some(Self::NormalTemporaryFailure),
246            486 | 600 => Some(Self::UserBusy),
247            484 => Some(Self::InvalidNumberFormat),
248            488 | 606 => Some(Self::IncompatibleDestination),
249            502 => Some(Self::NetworkOutOfOrder),
250            405 => Some(Self::ServiceUnavailable),
251            406 | 415 | 501 => Some(Self::ServiceNotImplemented),
252            482 | 483 => Some(Self::ExchangeRoutingError),
253            487 => Some(Self::OriginatorCancel),
254            428 => Some(Self::NoIdentity),
255            429 => Some(Self::BadIdentityInfo),
256            437 => Some(Self::UnsupportedCertificate),
257            438 => Some(Self::InvalidIdentity),
258            _ => None,
259        }
260    }
261}
262
263/// Channel timing data from FreeSWITCH's `switch_channel_timetable_t`.
264///
265/// Timestamps are epoch microseconds (`i64`). A value of `0` means the
266/// corresponding event never occurred (e.g., `hungup == Some(0)` means
267/// the channel has not hung up yet). `None` means the header was absent
268/// or unparseable.
269///
270/// Extracted from ESL event headers using a prefix (typically `"Caller"`
271/// or `"Other-Leg"`). The wire header format is `{prefix}-{suffix}`.
272///
273/// ## Headers extracted
274///
275/// `from_lookup()` extracts headers with suffixes from [`SUFFIXES`](Self::SUFFIXES).
276/// With `TimetablePrefix::Caller`, extracts `Caller-Channel-Created-Time` →
277/// `created`, `Caller-Channel-Hangup-Time` → `hungup`, etc.
278///
279/// Use `TimetablePrefix::OtherLeg` for `Other-Leg-*` headers,
280/// `TimetablePrefix::Channel` for outbound ESL `Channel-*` headers, or pass
281/// a custom string prefix to `from_lookup()`. See [`SUFFIXES`](Self::SUFFIXES)
282/// for the complete list.
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285#[non_exhaustive]
286pub struct ChannelTimetable {
287    /// When the caller profile was created.
288    pub profile_created: Option<i64>,
289    /// When the channel was created.
290    pub created: Option<i64>,
291    /// When the channel was answered.
292    pub answered: Option<i64>,
293    /// When early media (183) was received.
294    pub progress: Option<i64>,
295    /// When media-bearing early media arrived.
296    pub progress_media: Option<i64>,
297    /// When the channel hung up.
298    pub hungup: Option<i64>,
299    /// When the channel was transferred.
300    pub transferred: Option<i64>,
301    /// When the channel was resurrected.
302    pub resurrected: Option<i64>,
303    /// When the channel was bridged.
304    pub bridged: Option<i64>,
305    /// Timestamp of the last hold event.
306    pub last_hold: Option<i64>,
307    /// Accumulated hold time in microseconds.
308    pub hold_accum: Option<i64>,
309}
310
311/// Header prefix identifying which call leg's timetable to extract.
312///
313/// FreeSWITCH emits timetable headers as `{prefix}-Channel-Created-Time`, etc.
314/// The prefix varies by context -- `Caller` for the primary leg, `Other-Leg`
315/// for the bridged party, `Channel` in outbound ESL mode, etc.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
317#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
318#[non_exhaustive]
319pub enum TimetablePrefix {
320    /// Primary call leg (`Caller-*`).
321    Caller,
322    /// Bridged party (`Other-Leg-*`).
323    OtherLeg,
324    /// Outbound ESL channel profile (`Channel-*`).
325    Channel,
326    /// XML dialplan hunt (`Hunt-*`).
327    Hunt,
328    /// Bridge debug originator (`ORIGINATOR-*`).
329    Originator,
330    /// Bridge debug originatee (`ORIGINATEE-*`).
331    Originatee,
332    /// Post-bridge debug originator (`POST-ORIGINATOR-*`).
333    PostOriginator,
334    /// Post-bridge debug originatee (`POST-ORIGINATEE-*`).
335    PostOriginatee,
336}
337
338impl TimetablePrefix {
339    /// Wire-format prefix string.
340    pub fn as_str(&self) -> &'static str {
341        match self {
342            Self::Caller => "Caller",
343            Self::OtherLeg => "Other-Leg",
344            Self::Channel => "Channel",
345            Self::Hunt => "Hunt",
346            Self::Originator => "ORIGINATOR",
347            Self::Originatee => "ORIGINATEE",
348            Self::PostOriginator => "POST-ORIGINATOR",
349            Self::PostOriginatee => "POST-ORIGINATEE",
350        }
351    }
352}
353
354impl fmt::Display for TimetablePrefix {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        f.write_str(self.as_str())
357    }
358}
359
360impl AsRef<str> for TimetablePrefix {
361    fn as_ref(&self) -> &str {
362        self.as_str()
363    }
364}
365
366/// Error returned when a timetable header is present but not a valid `i64`.
367#[derive(Debug, Clone, PartialEq, Eq)]
368#[non_exhaustive]
369pub struct ParseTimetableError {
370    /// Full header name (e.g. `Caller-Channel-Created-Time`).
371    pub header: String,
372    /// The unparseable value found in the header.
373    pub value: String,
374}
375
376impl fmt::Display for ParseTimetableError {
377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378        write!(
379            f,
380            "invalid timetable value for {} ({} bytes)",
381            self.header,
382            self.value
383                .len()
384        )
385    }
386}
387
388impl std::error::Error for ParseTimetableError {}
389
390impl ParseTimetableError {
391    /// Create a new timetable parse error.
392    pub fn new(header: impl Into<String>, value: impl Into<String>) -> Self {
393        Self {
394            header: header.into(),
395            value: value.into(),
396        }
397    }
398}
399
400/// Generate [`TimetableField`], `ChannelTimetable::SUFFIXES` and
401/// `ChannelTimetable::from_lookup` from a single
402/// `(field => Variant => "Suffix")` table, eliminating the risk of drift between
403/// the named fields, the advertised suffix list and the headers actually
404/// extracted.
405macro_rules! channel_timetable_fields {
406    ($($field:ident => $Variant:ident => $suffix:literal),+ $(,)?) => {
407        /// One timetable header, named rather than positional.
408        ///
409        /// [`header_name()`](Self::header_name) composes the wire header a
410        /// consumer wanting a single timestamp has to subscribe to, filter on or
411        /// look up. The alternative is indexing
412        /// [`ChannelTimetable::SUFFIXES`](ChannelTimetable::SUFFIXES) by
413        /// position, which changes meaning if the table is ever reordered.
414        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
415        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
416        #[non_exhaustive]
417        pub enum TimetableField {
418            $(
419                #[doc = concat!("`", $suffix, "`, into [`ChannelTimetable::", stringify!($field), "`].")]
420                $Variant,
421            )+
422        }
423
424        impl TimetableField {
425            /// Header suffix, without any prefix.
426            pub fn as_str(&self) -> &'static str {
427                match self {
428                    $(Self::$Variant => $suffix,)+
429                }
430            }
431
432            /// Full wire header name for a call leg.
433            ///
434            /// Takes the same prefix forms as
435            /// [`ChannelTimetable::from_lookup`](ChannelTimetable::from_lookup):
436            /// a [`TimetablePrefix`] or a raw string for the dynamic `Call-<n>`
437            /// prefixes.
438            ///
439            /// ```
440            /// use freeswitch_types::{TimetableField, TimetablePrefix};
441            ///
442            /// assert_eq!(
443            ///     TimetableField::Answered.header_name(TimetablePrefix::OtherLeg),
444            ///     "Other-Leg-Channel-Answered-Time"
445            /// );
446            /// ```
447            pub fn header_name(&self, prefix: impl AsRef<str>) -> String {
448                format!("{}-{}", prefix.as_ref(), self.as_str())
449            }
450        }
451
452        impl ChannelTimetable {
453        /// Header suffixes extracted by `from_lookup()`.
454        ///
455        /// Combine with a `TimetablePrefix` to build full header names for event
456        /// subscriptions or filters:
457        ///
458        /// ```
459        /// use freeswitch_types::{ChannelTimetable, TimetablePrefix};
460        ///
461        /// let prefix = TimetablePrefix::Caller.as_str();
462        /// let headers: Vec<String> = ChannelTimetable::SUFFIXES
463        ///     .iter()
464        ///     .map(|suffix| format!("{prefix}-{suffix}"))
465        ///     .collect();
466        /// assert!(headers.contains(&"Caller-Channel-Created-Time".to_string()));
467        /// ```
468        pub const SUFFIXES: &'static [&'static str] = &[$($suffix),+];
469
470        /// Extract a timetable by looking up prefixed header names via a closure.
471        ///
472        /// The closure receives full header names (e.g. `"Caller-Channel-Created-Time"`)
473        /// and should return the raw value if present. Works with any key-value store:
474        /// `HashMap<String, String>`, `EslEvent`, `BTreeMap`, etc.
475        ///
476        /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
477        /// Returns `Err` if a header is present but contains an invalid (non-`i64`) value.
478        ///
479        /// ```
480        /// use std::collections::HashMap;
481        /// use freeswitch_types::{ChannelTimetable, TimetablePrefix};
482        ///
483        /// let mut headers: HashMap<String, String> = HashMap::new();
484        /// headers.insert("Caller-Channel-Created-Time".into(), "1700000001000000".into());
485        ///
486        /// // With enum:
487        /// let tt = ChannelTimetable::from_lookup(TimetablePrefix::Caller, |k| headers.get(k).map(|v: &String| v.as_str()));
488        /// assert!(tt.unwrap().unwrap().created.is_some());
489        ///
490        /// // With raw string (e.g. for dynamic "Call-1" prefix):
491        /// let tt = ChannelTimetable::from_lookup("Caller", |k| headers.get(k).map(|v: &String| v.as_str()));
492        /// assert!(tt.unwrap().unwrap().created.is_some());
493        /// ```
494        pub fn from_lookup<'a>(
495            prefix: impl AsRef<str>,
496            lookup: impl Fn(&str) -> Option<&'a str>,
497        ) -> Result<Option<Self>, ParseTimetableError> {
498            let prefix = prefix.as_ref();
499            let mut tt = Self::default();
500            let mut found = false;
501            $(
502                {
503                    let header = TimetableField::$Variant.header_name(prefix);
504                    if let Some(raw) = lookup(&header) {
505                        let v: i64 = raw.parse().map_err(|_| ParseTimetableError {
506                            header: header.clone(),
507                            value: raw.to_string(),
508                        })?;
509                        tt.$field = Some(v);
510                        found = true;
511                    }
512                }
513            )+
514            if found {
515                Ok(Some(tt))
516            } else {
517                Ok(None)
518            }
519        }
520        }
521    };
522}
523
524channel_timetable_fields! {
525    profile_created => ProfileCreated => "Profile-Created-Time",
526    created         => Created        => "Channel-Created-Time",
527    answered        => Answered       => "Channel-Answered-Time",
528    progress        => Progress       => "Channel-Progress-Time",
529    progress_media  => ProgressMedia  => "Channel-Progress-Media-Time",
530    hungup          => Hungup         => "Channel-Hangup-Time",
531    transferred     => Transferred    => "Channel-Transfer-Time",
532    resurrected     => Resurrected    => "Channel-Resurrect-Time",
533    bridged         => Bridged        => "Channel-Bridged-Time",
534    last_hold       => LastHold       => "Channel-Last-Hold",
535    hold_accum      => HoldAccum      => "Channel-Hold-Accum",
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::event::EslEvent;
542    use crate::lookup::HeaderLookup;
543
544    // --- ChannelState tests (display / from_str / wrong-case / unknown
545    //     are generated by the `tests: channel_state_wire_tests` clause
546    //     on the wire_enum! invocation above). ---
547
548    #[test]
549    fn channel_driver_is_the_segment_before_the_first_slash() {
550        assert_eq!(
551            channel_driver("sofia/internal/1000@example.com"),
552            Some("sofia")
553        );
554        assert_eq!(channel_driver("loopback/9199-a"), Some("loopback"));
555        assert_eq!(channel_driver("null/9199"), Some("null"));
556    }
557
558    #[test]
559    fn channel_driver_none_without_a_segment() {
560        for name in ["", "sofia", "/internal/1000"] {
561            assert_eq!(channel_driver(name), None, "{name:?}");
562        }
563    }
564
565    #[test]
566    fn timetable_error_display_omits_the_value() {
567        let err = ParseTimetableError::new("Caller-Channel-Created-Time", "5551234567");
568        let msg = err.to_string();
569        assert!(!msg.contains("5551234567"), "error quoted its input: {msg}");
570        assert!(msg.contains("Caller-Channel-Created-Time"), "{msg}");
571        assert_eq!(err.value, "5551234567");
572    }
573
574    #[test]
575    fn test_channel_state_from_number() {
576        assert_eq!(ChannelState::from_number(0), Some(ChannelState::CsNew));
577        assert_eq!(ChannelState::from_number(4), Some(ChannelState::CsExecute));
578        assert_eq!(ChannelState::from_number(10), Some(ChannelState::CsHangup));
579        assert_eq!(ChannelState::from_number(13), Some(ChannelState::CsNone));
580        assert_eq!(ChannelState::from_number(14), None);
581        assert_eq!(ChannelState::from_number(255), None);
582    }
583
584    #[test]
585    fn test_channel_state_as_number() {
586        assert_eq!(ChannelState::CsNew.as_number(), 0);
587        assert_eq!(ChannelState::CsExecute.as_number(), 4);
588        assert_eq!(ChannelState::CsHangup.as_number(), 10);
589        assert_eq!(ChannelState::CsNone.as_number(), 13);
590    }
591
592    #[test]
593    fn channel_state_ordering_follows_lifecycle() {
594        assert!(ChannelState::CsNew < ChannelState::CsInit);
595        assert!(ChannelState::CsInit < ChannelState::CsRouting);
596        assert!(ChannelState::CsRouting < ChannelState::CsExecute);
597        assert!(ChannelState::CsExecute < ChannelState::CsHangup);
598        assert!(ChannelState::CsHangup < ChannelState::CsReporting);
599        assert!(ChannelState::CsReporting < ChannelState::CsDestroy);
600    }
601
602    // Negated >= reads as "not yet in teardown", which is the real consumer intent.
603    #[allow(clippy::nonminimal_bool)]
604    #[test]
605    fn channel_state_teardown_check() {
606        assert!(ChannelState::CsHangup >= ChannelState::CsHangup);
607        assert!(ChannelState::CsReporting >= ChannelState::CsHangup);
608        assert!(ChannelState::CsDestroy >= ChannelState::CsHangup);
609        assert!(!(ChannelState::CsExecute >= ChannelState::CsHangup));
610        assert!(!(ChannelState::CsPark >= ChannelState::CsHangup));
611    }
612
613    #[test]
614    fn only_destroy_is_terminal() {
615        assert!(ChannelState::CsDestroy.is_terminal());
616        for state in [
617            ChannelState::CsNew,
618            ChannelState::CsInit,
619            ChannelState::CsRouting,
620            ChannelState::CsSoftExecute,
621            ChannelState::CsExecute,
622            ChannelState::CsExchangeMedia,
623            ChannelState::CsPark,
624            ChannelState::CsConsumeMedia,
625            ChannelState::CsHibernate,
626            ChannelState::CsReset,
627            ChannelState::CsHangup,
628            ChannelState::CsReporting,
629            ChannelState::CsNone,
630        ] {
631            assert!(!state.is_terminal(), "{state} is not an end of life");
632        }
633    }
634
635    // CS_NONE sorts above CS_DESTROY, so the `>= CsHangup` idiom above sweeps it
636    // in; the switch aborts if its state machine ever reaches it, so it ends
637    // nothing.
638    #[test]
639    fn cs_none_outranks_destroy_without_being_terminal() {
640        assert!(ChannelState::CsNone > ChannelState::CsDestroy);
641        assert!(ChannelState::CsNone >= ChannelState::CsHangup);
642        assert!(!ChannelState::CsNone.is_terminal());
643    }
644
645    // --- CallState tests (display / from_str / wrong-case / unknown are
646    //     generated by the `tests: call_state_wire_tests` clause on the
647    //     wire_enum! invocation above; only the ordering check stays
648    //     manual because it depends on enum-lexical order, not wire
649    //     format). ---
650
651    #[test]
652    fn call_state_ordering_matches_c_enum() {
653        assert!(CallState::Down < CallState::Dialing);
654        assert!(CallState::Dialing < CallState::Ringing);
655        assert!(CallState::Early < CallState::Active);
656        assert!(CallState::Active < CallState::Hangup);
657    }
658
659    // --- AnswerState / CallDirection wire tests are generated by
660    //     `tests: answer_state_wire_tests` / `tests: call_direction_wire_tests`
661    //     clauses on their wire_enum! invocations above. ---
662
663    // --- HangupCause tests ---
664
665    #[test]
666    fn hangup_cause_display() {
667        assert_eq!(HangupCause::NormalClearing.to_string(), "NORMAL_CLEARING");
668        assert_eq!(HangupCause::UserBusy.to_string(), "USER_BUSY");
669        assert_eq!(
670            HangupCause::OriginatorCancel.to_string(),
671            "ORIGINATOR_CANCEL"
672        );
673        assert_eq!(HangupCause::None.to_string(), "NONE");
674    }
675
676    #[test]
677    fn hangup_cause_from_str() {
678        assert_eq!(
679            "NORMAL_CLEARING"
680                .parse::<HangupCause>()
681                .unwrap(),
682            HangupCause::NormalClearing
683        );
684        assert_eq!(
685            "USER_BUSY"
686                .parse::<HangupCause>()
687                .unwrap(),
688            HangupCause::UserBusy
689        );
690    }
691
692    #[test]
693    fn hangup_cause_from_str_rejects_wrong_case() {
694        assert!("normal_clearing"
695            .parse::<HangupCause>()
696            .is_err());
697        assert!("User_Busy"
698            .parse::<HangupCause>()
699            .is_err());
700    }
701
702    #[test]
703    fn hangup_cause_from_str_unknown() {
704        assert!("BOGUS_CAUSE"
705            .parse::<HangupCause>()
706            .is_err());
707    }
708
709    #[test]
710    fn hangup_cause_display_round_trip() {
711        let causes = [
712            HangupCause::None,
713            HangupCause::NormalClearing,
714            HangupCause::UserBusy,
715            HangupCause::NoAnswer,
716            HangupCause::OriginatorCancel,
717            HangupCause::BlindTransfer,
718            HangupCause::InvalidIdentity,
719        ];
720        for cause in causes {
721            let s = cause.to_string();
722            let parsed: HangupCause = s
723                .parse()
724                .unwrap();
725            assert_eq!(parsed, cause);
726        }
727    }
728
729    #[test]
730    fn hangup_cause_as_number_q850() {
731        assert_eq!(HangupCause::None.as_number(), 0);
732        assert_eq!(HangupCause::UnallocatedNumber.as_number(), 1);
733        assert_eq!(HangupCause::NormalClearing.as_number(), 16);
734        assert_eq!(HangupCause::UserBusy.as_number(), 17);
735        assert_eq!(HangupCause::NoAnswer.as_number(), 19);
736        assert_eq!(HangupCause::CallRejected.as_number(), 21);
737        assert_eq!(HangupCause::NormalUnspecified.as_number(), 31);
738        assert_eq!(HangupCause::Interworking.as_number(), 127);
739    }
740
741    #[test]
742    fn hangup_cause_as_number_freeswitch_extensions() {
743        assert_eq!(HangupCause::Success.as_number(), 142);
744        assert_eq!(HangupCause::OriginatorCancel.as_number(), 487);
745        assert_eq!(HangupCause::Crash.as_number(), 700);
746        assert_eq!(HangupCause::BlindTransfer.as_number(), 800);
747        assert_eq!(HangupCause::InvalidIdentity.as_number(), 823);
748    }
749
750    #[test]
751    fn hangup_cause_from_number_round_trip() {
752        let codes: &[u16] = &[0, 1, 16, 17, 19, 21, 31, 127, 142, 487, 700, 800, 823];
753        for &code in codes {
754            let cause = HangupCause::from_number(code).unwrap();
755            assert_eq!(cause.as_number(), code);
756        }
757    }
758
759    #[test]
760    fn hangup_cause_from_number_unknown() {
761        assert!(HangupCause::from_number(999).is_none());
762        assert!(HangupCause::from_number(4).is_none());
763    }
764
765    // --- from_sip_response tests (mapping from sofia_glue_sip_cause_to_freeswitch) ---
766
767    #[test]
768    fn from_sip_response_success() {
769        assert_eq!(
770            HangupCause::from_sip_response(200),
771            Some(HangupCause::NormalClearing)
772        );
773    }
774
775    #[test]
776    fn from_sip_response_4xx_auth_rejection() {
777        for code in [401, 402, 403, 407] {
778            assert_eq!(
779                HangupCause::from_sip_response(code),
780                Some(HangupCause::CallRejected),
781                "SIP {code}"
782            );
783        }
784    }
785
786    #[test]
787    fn from_sip_response_4xx_routing() {
788        assert_eq!(
789            HangupCause::from_sip_response(404),
790            Some(HangupCause::UnallocatedNumber)
791        );
792        assert_eq!(
793            HangupCause::from_sip_response(485),
794            Some(HangupCause::NoRouteDestination)
795        );
796        assert_eq!(
797            HangupCause::from_sip_response(484),
798            Some(HangupCause::InvalidNumberFormat)
799        );
800        assert_eq!(
801            HangupCause::from_sip_response(410),
802            Some(HangupCause::NumberChanged)
803        );
804    }
805
806    #[test]
807    fn from_sip_response_4xx_service() {
808        assert_eq!(
809            HangupCause::from_sip_response(405),
810            Some(HangupCause::ServiceUnavailable)
811        );
812        for code in [406, 415, 501] {
813            assert_eq!(
814                HangupCause::from_sip_response(code),
815                Some(HangupCause::ServiceNotImplemented),
816                "SIP {code}"
817            );
818        }
819    }
820
821    #[test]
822    fn from_sip_response_4xx_interworking() {
823        for code in [413, 414, 416, 420, 421, 423, 505, 513] {
824            assert_eq!(
825                HangupCause::from_sip_response(code),
826                Some(HangupCause::Interworking),
827                "SIP {code}"
828            );
829        }
830    }
831
832    #[test]
833    fn from_sip_response_4xx_timeout_and_busy() {
834        assert_eq!(
835            HangupCause::from_sip_response(408),
836            Some(HangupCause::RecoveryOnTimerExpire)
837        );
838        assert_eq!(
839            HangupCause::from_sip_response(504),
840            Some(HangupCause::RecoveryOnTimerExpire)
841        );
842        assert_eq!(
843            HangupCause::from_sip_response(480),
844            Some(HangupCause::NoUserResponse)
845        );
846        assert_eq!(
847            HangupCause::from_sip_response(486),
848            Some(HangupCause::UserBusy)
849        );
850        assert_eq!(
851            HangupCause::from_sip_response(487),
852            Some(HangupCause::OriginatorCancel)
853        );
854    }
855
856    #[test]
857    fn from_sip_response_4xx_temporary_failure() {
858        for code in [400, 481, 500, 503] {
859            assert_eq!(
860                HangupCause::from_sip_response(code),
861                Some(HangupCause::NormalTemporaryFailure),
862                "SIP {code}"
863            );
864        }
865    }
866
867    #[test]
868    fn from_sip_response_4xx_exchange_routing() {
869        for code in [482, 483] {
870            assert_eq!(
871                HangupCause::from_sip_response(code),
872                Some(HangupCause::ExchangeRoutingError),
873                "SIP {code}"
874            );
875        }
876    }
877
878    #[test]
879    fn from_sip_response_4xx_media() {
880        assert_eq!(
881            HangupCause::from_sip_response(488),
882            Some(HangupCause::IncompatibleDestination)
883        );
884        assert_eq!(
885            HangupCause::from_sip_response(606),
886            Some(HangupCause::IncompatibleDestination)
887        );
888    }
889
890    #[test]
891    fn from_sip_response_5xx() {
892        assert_eq!(
893            HangupCause::from_sip_response(502),
894            Some(HangupCause::NetworkOutOfOrder)
895        );
896    }
897
898    #[test]
899    fn from_sip_response_6xx() {
900        assert_eq!(
901            HangupCause::from_sip_response(600),
902            Some(HangupCause::UserBusy)
903        );
904        assert_eq!(
905            HangupCause::from_sip_response(603),
906            Some(HangupCause::CallRejected)
907        );
908        assert_eq!(
909            HangupCause::from_sip_response(604),
910            Some(HangupCause::NoRouteDestination)
911        );
912        assert_eq!(
913            HangupCause::from_sip_response(607),
914            Some(HangupCause::Unwanted)
915        );
916        assert_eq!(
917            HangupCause::from_sip_response(608),
918            Some(HangupCause::CallRejected)
919        );
920    }
921
922    #[test]
923    fn from_sip_response_stir_shaken() {
924        assert_eq!(
925            HangupCause::from_sip_response(428),
926            Some(HangupCause::NoIdentity)
927        );
928        assert_eq!(
929            HangupCause::from_sip_response(429),
930            Some(HangupCause::BadIdentityInfo)
931        );
932        assert_eq!(
933            HangupCause::from_sip_response(437),
934            Some(HangupCause::UnsupportedCertificate)
935        );
936        assert_eq!(
937            HangupCause::from_sip_response(438),
938            Some(HangupCause::InvalidIdentity)
939        );
940    }
941
942    #[test]
943    fn from_sip_response_unmapped_returns_none() {
944        // Provisional/success ranges without explicit mapping
945        for code in [100, 180, 183, 301, 302] {
946            assert_eq!(
947                HangupCause::from_sip_response(code),
948                None,
949                "SIP {code} should be None"
950            );
951        }
952        // Unmapped 4xx/5xx
953        for code in [409, 411, 412, 422, 489, 491, 493, 506, 580] {
954            assert_eq!(
955                HangupCause::from_sip_response(code),
956                None,
957                "SIP {code} should be None"
958            );
959        }
960    }
961
962    // --- ChannelTimetable tests ---
963
964    #[test]
965    fn caller_timetable_all_fields() {
966        let mut event = EslEvent::new();
967        event.set_header("Caller-Profile-Created-Time", "1700000000000000");
968        event.set_header("Caller-Channel-Created-Time", "1700000001000000");
969        event.set_header("Caller-Channel-Answered-Time", "1700000005000000");
970        event.set_header("Caller-Channel-Progress-Time", "1700000002000000");
971        event.set_header("Caller-Channel-Progress-Media-Time", "1700000003000000");
972        event.set_header("Caller-Channel-Hangup-Time", "0");
973        event.set_header("Caller-Channel-Transfer-Time", "0");
974        event.set_header("Caller-Channel-Resurrect-Time", "0");
975        event.set_header("Caller-Channel-Bridged-Time", "1700000006000000");
976        event.set_header("Caller-Channel-Last-Hold", "0");
977        event.set_header("Caller-Channel-Hold-Accum", "0");
978
979        let tt = event
980            .caller_timetable()
981            .unwrap()
982            .expect("should have timetable");
983        assert_eq!(tt.profile_created, Some(1700000000000000));
984        assert_eq!(tt.created, Some(1700000001000000));
985        assert_eq!(tt.answered, Some(1700000005000000));
986        assert_eq!(tt.progress, Some(1700000002000000));
987        assert_eq!(tt.progress_media, Some(1700000003000000));
988        assert_eq!(tt.hungup, Some(0));
989        assert_eq!(tt.transferred, Some(0));
990        assert_eq!(tt.resurrected, Some(0));
991        assert_eq!(tt.bridged, Some(1700000006000000));
992        assert_eq!(tt.last_hold, Some(0));
993        assert_eq!(tt.hold_accum, Some(0));
994    }
995
996    #[test]
997    fn other_leg_timetable() {
998        let mut event = EslEvent::new();
999        event.set_header("Other-Leg-Profile-Created-Time", "1700000000000000");
1000        event.set_header("Other-Leg-Channel-Created-Time", "1700000001000000");
1001        event.set_header("Other-Leg-Channel-Answered-Time", "1700000005000000");
1002        event.set_header("Other-Leg-Channel-Progress-Time", "0");
1003        event.set_header("Other-Leg-Channel-Progress-Media-Time", "0");
1004        event.set_header("Other-Leg-Channel-Hangup-Time", "0");
1005        event.set_header("Other-Leg-Channel-Transfer-Time", "0");
1006        event.set_header("Other-Leg-Channel-Resurrect-Time", "0");
1007        event.set_header("Other-Leg-Channel-Bridged-Time", "1700000006000000");
1008        event.set_header("Other-Leg-Channel-Last-Hold", "0");
1009        event.set_header("Other-Leg-Channel-Hold-Accum", "0");
1010
1011        let tt = event
1012            .other_leg_timetable()
1013            .unwrap()
1014            .expect("should have timetable");
1015        assert_eq!(tt.created, Some(1700000001000000));
1016        assert_eq!(tt.bridged, Some(1700000006000000));
1017    }
1018
1019    #[test]
1020    fn timetable_no_headers() {
1021        let event = EslEvent::new();
1022        assert_eq!(
1023            event
1024                .caller_timetable()
1025                .unwrap(),
1026            None
1027        );
1028        assert_eq!(
1029            event
1030                .other_leg_timetable()
1031                .unwrap(),
1032            None
1033        );
1034    }
1035
1036    #[test]
1037    fn timetable_partial_headers() {
1038        let mut event = EslEvent::new();
1039        event.set_header("Caller-Channel-Created-Time", "1700000001000000");
1040
1041        let tt = event
1042            .caller_timetable()
1043            .unwrap()
1044            .expect("at least one field parsed");
1045        assert_eq!(tt.created, Some(1700000001000000));
1046        assert_eq!(tt.answered, None);
1047        assert_eq!(tt.profile_created, None);
1048    }
1049
1050    #[test]
1051    fn timetable_invalid_value_is_error() {
1052        let mut event = EslEvent::new();
1053        event.set_header("Caller-Channel-Created-Time", "not_a_number");
1054
1055        let err = event
1056            .caller_timetable()
1057            .unwrap_err();
1058        assert_eq!(err.header, "Caller-Channel-Created-Time");
1059        assert_eq!(err.value, "not_a_number");
1060    }
1061
1062    #[test]
1063    fn timetable_valid_then_invalid_is_error() {
1064        let mut event = EslEvent::new();
1065        event.set_header("Caller-Profile-Created-Time", "1700000000000000");
1066        event.set_header("Caller-Channel-Created-Time", "garbage");
1067
1068        let err = event
1069            .caller_timetable()
1070            .unwrap_err();
1071        assert_eq!(err.header, "Caller-Channel-Created-Time");
1072        assert_eq!(err.value, "garbage");
1073    }
1074
1075    #[test]
1076    fn timetable_zero_preserved() {
1077        let mut event = EslEvent::new();
1078        event.set_header("Caller-Channel-Hangup-Time", "0");
1079
1080        let tt = event
1081            .caller_timetable()
1082            .unwrap()
1083            .expect("should have timetable");
1084        assert_eq!(tt.hungup, Some(0));
1085    }
1086
1087    #[test]
1088    fn timetable_field_names_one_header() {
1089        assert_eq!(
1090            TimetableField::ProfileCreated.as_str(),
1091            "Profile-Created-Time"
1092        );
1093        assert_eq!(
1094            TimetableField::Created.header_name(TimetablePrefix::Caller),
1095            "Caller-Channel-Created-Time"
1096        );
1097        // Raw prefixes too, for the dynamic `Call-<n>` form from_lookup accepts.
1098        assert_eq!(
1099            TimetableField::Hungup.header_name("Call-1"),
1100            "Call-1-Channel-Hangup-Time"
1101        );
1102    }
1103
1104    #[test]
1105    fn timetable_fields_and_suffixes_come_from_one_table() {
1106        let fields = [
1107            TimetableField::ProfileCreated,
1108            TimetableField::Created,
1109            TimetableField::Answered,
1110            TimetableField::Progress,
1111            TimetableField::ProgressMedia,
1112            TimetableField::Hungup,
1113            TimetableField::Transferred,
1114            TimetableField::Resurrected,
1115            TimetableField::Bridged,
1116            TimetableField::LastHold,
1117            TimetableField::HoldAccum,
1118        ];
1119        assert_eq!(fields.len(), ChannelTimetable::SUFFIXES.len());
1120        for (field, suffix) in fields
1121            .iter()
1122            .zip(ChannelTimetable::SUFFIXES)
1123        {
1124            assert_eq!(field.as_str(), *suffix);
1125        }
1126    }
1127
1128    #[test]
1129    fn timetable_field_header_name_is_what_from_lookup_reads() {
1130        let mut event = EslEvent::new();
1131        event.set_header(
1132            TimetableField::Bridged.header_name(TimetablePrefix::OtherLeg),
1133            "1700000006000000",
1134        );
1135
1136        let tt = event
1137            .other_leg_timetable()
1138            .unwrap()
1139            .expect("should have timetable");
1140        assert_eq!(tt.bridged, Some(1700000006000000));
1141    }
1142
1143    #[test]
1144    fn timetable_custom_prefix() {
1145        let mut event = EslEvent::new();
1146        event.set_header("Channel-Channel-Created-Time", "1700000001000000");
1147
1148        let tt = event
1149            .timetable("Channel")
1150            .unwrap()
1151            .expect("custom prefix should work");
1152        assert_eq!(tt.created, Some(1700000001000000));
1153    }
1154}