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