Skip to main content

freeswitch_types/
channel.rs

1//! Channel-related data types extracted from ESL event headers.
2
3use std::fmt;
4use std::str::FromStr;
5
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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[non_exhaustive]
11#[repr(u8)]
12#[allow(missing_docs)]
13pub enum ChannelState {
14    CsNew = 0,
15    CsInit = 1,
16    CsRouting = 2,
17    CsSoftExecute = 3,
18    CsExecute = 4,
19    CsExchangeMedia = 5,
20    CsPark = 6,
21    CsConsumeMedia = 7,
22    CsHibernate = 8,
23    CsReset = 9,
24    CsHangup = 10,
25    CsReporting = 11,
26    CsDestroy = 12,
27    CsNone = 13,
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
58impl fmt::Display for ChannelState {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        let name = match self {
61            Self::CsNew => "CS_NEW",
62            Self::CsInit => "CS_INIT",
63            Self::CsRouting => "CS_ROUTING",
64            Self::CsSoftExecute => "CS_SOFT_EXECUTE",
65            Self::CsExecute => "CS_EXECUTE",
66            Self::CsExchangeMedia => "CS_EXCHANGE_MEDIA",
67            Self::CsPark => "CS_PARK",
68            Self::CsConsumeMedia => "CS_CONSUME_MEDIA",
69            Self::CsHibernate => "CS_HIBERNATE",
70            Self::CsReset => "CS_RESET",
71            Self::CsHangup => "CS_HANGUP",
72            Self::CsReporting => "CS_REPORTING",
73            Self::CsDestroy => "CS_DESTROY",
74            Self::CsNone => "CS_NONE",
75        };
76        f.write_str(name)
77    }
78}
79
80/// Error returned when parsing an invalid channel state string.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ParseChannelStateError(pub String);
83
84impl fmt::Display for ParseChannelStateError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "unknown channel state: {}", self.0)
87    }
88}
89
90impl std::error::Error for ParseChannelStateError {}
91
92impl FromStr for ChannelState {
93    type Err = ParseChannelStateError;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        match s {
97            "CS_NEW" => Ok(Self::CsNew),
98            "CS_INIT" => Ok(Self::CsInit),
99            "CS_ROUTING" => Ok(Self::CsRouting),
100            "CS_SOFT_EXECUTE" => Ok(Self::CsSoftExecute),
101            "CS_EXECUTE" => Ok(Self::CsExecute),
102            "CS_EXCHANGE_MEDIA" => Ok(Self::CsExchangeMedia),
103            "CS_PARK" => Ok(Self::CsPark),
104            "CS_CONSUME_MEDIA" => Ok(Self::CsConsumeMedia),
105            "CS_HIBERNATE" => Ok(Self::CsHibernate),
106            "CS_RESET" => Ok(Self::CsReset),
107            "CS_HANGUP" => Ok(Self::CsHangup),
108            "CS_REPORTING" => Ok(Self::CsReporting),
109            "CS_DESTROY" => Ok(Self::CsDestroy),
110            "CS_NONE" => Ok(Self::CsNone),
111            _ => Err(ParseChannelStateError(s.to_string())),
112        }
113    }
114}
115
116/// Call state from `switch_channel_callstate_t` -- carried in the `Channel-Call-State` header.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119#[non_exhaustive]
120#[allow(missing_docs)]
121pub enum CallState {
122    Down,
123    Dialing,
124    Ringing,
125    Early,
126    Active,
127    Held,
128    RingWait,
129    Hangup,
130    Unheld,
131}
132
133impl fmt::Display for CallState {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        let name = match self {
136            Self::Down => "DOWN",
137            Self::Dialing => "DIALING",
138            Self::Ringing => "RINGING",
139            Self::Early => "EARLY",
140            Self::Active => "ACTIVE",
141            Self::Held => "HELD",
142            Self::RingWait => "RING_WAIT",
143            Self::Hangup => "HANGUP",
144            Self::Unheld => "UNHELD",
145        };
146        f.write_str(name)
147    }
148}
149
150/// Error returned when parsing an invalid call state string.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ParseCallStateError(pub String);
153
154impl fmt::Display for ParseCallStateError {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "unknown call state: {}", self.0)
157    }
158}
159
160impl std::error::Error for ParseCallStateError {}
161
162impl FromStr for CallState {
163    type Err = ParseCallStateError;
164
165    fn from_str(s: &str) -> Result<Self, Self::Err> {
166        match s {
167            "DOWN" => Ok(Self::Down),
168            "DIALING" => Ok(Self::Dialing),
169            "RINGING" => Ok(Self::Ringing),
170            "EARLY" => Ok(Self::Early),
171            "ACTIVE" => Ok(Self::Active),
172            "HELD" => Ok(Self::Held),
173            "RING_WAIT" => Ok(Self::RingWait),
174            "HANGUP" => Ok(Self::Hangup),
175            "UNHELD" => Ok(Self::Unheld),
176            _ => Err(ParseCallStateError(s.to_string())),
177        }
178    }
179}
180
181/// Answer state from the `Answer-State` header. Wire format is lowercase.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184#[non_exhaustive]
185#[allow(missing_docs)]
186pub enum AnswerState {
187    Hangup,
188    Answered,
189    Early,
190    Ringing,
191}
192
193impl fmt::Display for AnswerState {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        let name = match self {
196            Self::Hangup => "hangup",
197            Self::Answered => "answered",
198            Self::Early => "early",
199            Self::Ringing => "ringing",
200        };
201        f.write_str(name)
202    }
203}
204
205/// Error returned when parsing an invalid answer state string.
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct ParseAnswerStateError(pub String);
208
209impl fmt::Display for ParseAnswerStateError {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "unknown answer state: {}", self.0)
212    }
213}
214
215impl std::error::Error for ParseAnswerStateError {}
216
217impl FromStr for AnswerState {
218    type Err = ParseAnswerStateError;
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        match s {
222            "hangup" => Ok(Self::Hangup),
223            "answered" => Ok(Self::Answered),
224            "early" => Ok(Self::Early),
225            "ringing" => Ok(Self::Ringing),
226            _ => Err(ParseAnswerStateError(s.to_string())),
227        }
228    }
229}
230
231/// Call direction from the `Call-Direction` header. Wire format is lowercase.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234#[non_exhaustive]
235#[allow(missing_docs)]
236pub enum CallDirection {
237    Inbound,
238    Outbound,
239}
240
241impl fmt::Display for CallDirection {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        let name = match self {
244            Self::Inbound => "inbound",
245            Self::Outbound => "outbound",
246        };
247        f.write_str(name)
248    }
249}
250
251/// Error returned when parsing an invalid call direction string.
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ParseCallDirectionError(pub String);
254
255impl fmt::Display for ParseCallDirectionError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "unknown call direction: {}", self.0)
258    }
259}
260
261impl std::error::Error for ParseCallDirectionError {}
262
263impl FromStr for CallDirection {
264    type Err = ParseCallDirectionError;
265
266    fn from_str(s: &str) -> Result<Self, Self::Err> {
267        match s {
268            "inbound" => Ok(Self::Inbound),
269            "outbound" => Ok(Self::Outbound),
270            _ => Err(ParseCallDirectionError(s.to_string())),
271        }
272    }
273}
274
275/// Error returned when parsing an unknown hangup cause string.
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct ParseHangupCauseError(pub String);
278
279impl fmt::Display for ParseHangupCauseError {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        write!(f, "unknown hangup cause: {}", self.0)
282    }
283}
284
285impl std::error::Error for ParseHangupCauseError {}
286
287/// Hangup cause from `switch_cause_t` (Q.850 + FreeSWITCH extensions).
288///
289/// Carried in the `Hangup-Cause` header. Wire format is `SCREAMING_SNAKE_CASE`
290/// (e.g. `NORMAL_CLEARING`). The numeric value matches the Q.850 cause code
291/// for standard causes, or a FreeSWITCH-internal range for extensions.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294#[non_exhaustive]
295#[repr(u16)]
296#[allow(missing_docs)]
297pub enum HangupCause {
298    None = 0,
299    UnallocatedNumber = 1,
300    NoRouteTransitNet = 2,
301    NoRouteDestination = 3,
302    ChannelUnacceptable = 6,
303    CallAwardedDelivered = 7,
304    NormalClearing = 16,
305    UserBusy = 17,
306    NoUserResponse = 18,
307    NoAnswer = 19,
308    SubscriberAbsent = 20,
309    CallRejected = 21,
310    NumberChanged = 22,
311    RedirectionToNewDestination = 23,
312    ExchangeRoutingError = 25,
313    DestinationOutOfOrder = 27,
314    InvalidNumberFormat = 28,
315    FacilityRejected = 29,
316    ResponseToStatusEnquiry = 30,
317    NormalUnspecified = 31,
318    NormalCircuitCongestion = 34,
319    NetworkOutOfOrder = 38,
320    NormalTemporaryFailure = 41,
321    SwitchCongestion = 42,
322    AccessInfoDiscarded = 43,
323    RequestedChanUnavail = 44,
324    PreEmpted = 45,
325    FacilityNotSubscribed = 50,
326    OutgoingCallBarred = 52,
327    IncomingCallBarred = 54,
328    BearercapabilityNotauth = 57,
329    BearercapabilityNotavail = 58,
330    ServiceUnavailable = 63,
331    BearercapabilityNotimpl = 65,
332    ChanNotImplemented = 66,
333    FacilityNotImplemented = 69,
334    ServiceNotImplemented = 79,
335    InvalidCallReference = 81,
336    IncompatibleDestination = 88,
337    InvalidMsgUnspecified = 95,
338    MandatoryIeMissing = 96,
339    MessageTypeNonexist = 97,
340    WrongMessage = 98,
341    IeNonexist = 99,
342    InvalidIeContents = 100,
343    WrongCallState = 101,
344    RecoveryOnTimerExpire = 102,
345    MandatoryIeLengthError = 103,
346    ProtocolError = 111,
347    Interworking = 127,
348    Success = 142,
349    OriginatorCancel = 487,
350    Crash = 700,
351    SystemShutdown = 701,
352    LoseRace = 702,
353    ManagerRequest = 703,
354    BlindTransfer = 800,
355    AttendedTransfer = 801,
356    AllottedTimeout = 802,
357    UserChallenge = 803,
358    MediaTimeout = 804,
359    PickedOff = 805,
360    UserNotRegistered = 806,
361    ProgressTimeout = 807,
362    InvalidGateway = 808,
363    GatewayDown = 809,
364    InvalidUrl = 810,
365    InvalidProfile = 811,
366    NoPickup = 812,
367    SrtpReadError = 813,
368    Bowout = 814,
369    BusyEverywhere = 815,
370    Decline = 816,
371    DoesNotExistAnywhere = 817,
372    NotAcceptable = 818,
373    Unwanted = 819,
374    NoIdentity = 820,
375    BadIdentityInfo = 821,
376    UnsupportedCertificate = 822,
377    InvalidIdentity = 823,
378    /// Stale Date (STIR/SHAKEN).
379    StaleDate = 824,
380    /// Reject all calls.
381    RejectAll = 825,
382}
383
384impl HangupCause {
385    /// Q.850 / FreeSWITCH numeric cause code.
386    pub fn as_number(&self) -> u16 {
387        *self as u16
388    }
389
390    /// Look up by numeric cause code.
391    pub fn from_number(n: u16) -> Option<Self> {
392        match n {
393            0 => Some(Self::None),
394            1 => Some(Self::UnallocatedNumber),
395            2 => Some(Self::NoRouteTransitNet),
396            3 => Some(Self::NoRouteDestination),
397            6 => Some(Self::ChannelUnacceptable),
398            7 => Some(Self::CallAwardedDelivered),
399            16 => Some(Self::NormalClearing),
400            17 => Some(Self::UserBusy),
401            18 => Some(Self::NoUserResponse),
402            19 => Some(Self::NoAnswer),
403            20 => Some(Self::SubscriberAbsent),
404            21 => Some(Self::CallRejected),
405            22 => Some(Self::NumberChanged),
406            23 => Some(Self::RedirectionToNewDestination),
407            25 => Some(Self::ExchangeRoutingError),
408            27 => Some(Self::DestinationOutOfOrder),
409            28 => Some(Self::InvalidNumberFormat),
410            29 => Some(Self::FacilityRejected),
411            30 => Some(Self::ResponseToStatusEnquiry),
412            31 => Some(Self::NormalUnspecified),
413            34 => Some(Self::NormalCircuitCongestion),
414            38 => Some(Self::NetworkOutOfOrder),
415            41 => Some(Self::NormalTemporaryFailure),
416            42 => Some(Self::SwitchCongestion),
417            43 => Some(Self::AccessInfoDiscarded),
418            44 => Some(Self::RequestedChanUnavail),
419            45 => Some(Self::PreEmpted),
420            50 => Some(Self::FacilityNotSubscribed),
421            52 => Some(Self::OutgoingCallBarred),
422            54 => Some(Self::IncomingCallBarred),
423            57 => Some(Self::BearercapabilityNotauth),
424            58 => Some(Self::BearercapabilityNotavail),
425            63 => Some(Self::ServiceUnavailable),
426            65 => Some(Self::BearercapabilityNotimpl),
427            66 => Some(Self::ChanNotImplemented),
428            69 => Some(Self::FacilityNotImplemented),
429            79 => Some(Self::ServiceNotImplemented),
430            81 => Some(Self::InvalidCallReference),
431            88 => Some(Self::IncompatibleDestination),
432            95 => Some(Self::InvalidMsgUnspecified),
433            96 => Some(Self::MandatoryIeMissing),
434            97 => Some(Self::MessageTypeNonexist),
435            98 => Some(Self::WrongMessage),
436            99 => Some(Self::IeNonexist),
437            100 => Some(Self::InvalidIeContents),
438            101 => Some(Self::WrongCallState),
439            102 => Some(Self::RecoveryOnTimerExpire),
440            103 => Some(Self::MandatoryIeLengthError),
441            111 => Some(Self::ProtocolError),
442            127 => Some(Self::Interworking),
443            142 => Some(Self::Success),
444            487 => Some(Self::OriginatorCancel),
445            700 => Some(Self::Crash),
446            701 => Some(Self::SystemShutdown),
447            702 => Some(Self::LoseRace),
448            703 => Some(Self::ManagerRequest),
449            800 => Some(Self::BlindTransfer),
450            801 => Some(Self::AttendedTransfer),
451            802 => Some(Self::AllottedTimeout),
452            803 => Some(Self::UserChallenge),
453            804 => Some(Self::MediaTimeout),
454            805 => Some(Self::PickedOff),
455            806 => Some(Self::UserNotRegistered),
456            807 => Some(Self::ProgressTimeout),
457            808 => Some(Self::InvalidGateway),
458            809 => Some(Self::GatewayDown),
459            810 => Some(Self::InvalidUrl),
460            811 => Some(Self::InvalidProfile),
461            812 => Some(Self::NoPickup),
462            813 => Some(Self::SrtpReadError),
463            814 => Some(Self::Bowout),
464            815 => Some(Self::BusyEverywhere),
465            816 => Some(Self::Decline),
466            817 => Some(Self::DoesNotExistAnywhere),
467            818 => Some(Self::NotAcceptable),
468            819 => Some(Self::Unwanted),
469            820 => Some(Self::NoIdentity),
470            821 => Some(Self::BadIdentityInfo),
471            822 => Some(Self::UnsupportedCertificate),
472            823 => Some(Self::InvalidIdentity),
473            824 => Some(Self::StaleDate),
474            825 => Some(Self::RejectAll),
475            _ => None,
476        }
477    }
478}
479
480impl fmt::Display for HangupCause {
481    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482        let name = match self {
483            Self::None => "NONE",
484            Self::UnallocatedNumber => "UNALLOCATED_NUMBER",
485            Self::NoRouteTransitNet => "NO_ROUTE_TRANSIT_NET",
486            Self::NoRouteDestination => "NO_ROUTE_DESTINATION",
487            Self::ChannelUnacceptable => "CHANNEL_UNACCEPTABLE",
488            Self::CallAwardedDelivered => "CALL_AWARDED_DELIVERED",
489            Self::NormalClearing => "NORMAL_CLEARING",
490            Self::UserBusy => "USER_BUSY",
491            Self::NoUserResponse => "NO_USER_RESPONSE",
492            Self::NoAnswer => "NO_ANSWER",
493            Self::SubscriberAbsent => "SUBSCRIBER_ABSENT",
494            Self::CallRejected => "CALL_REJECTED",
495            Self::NumberChanged => "NUMBER_CHANGED",
496            Self::RedirectionToNewDestination => "REDIRECTION_TO_NEW_DESTINATION",
497            Self::ExchangeRoutingError => "EXCHANGE_ROUTING_ERROR",
498            Self::DestinationOutOfOrder => "DESTINATION_OUT_OF_ORDER",
499            Self::InvalidNumberFormat => "INVALID_NUMBER_FORMAT",
500            Self::FacilityRejected => "FACILITY_REJECTED",
501            Self::ResponseToStatusEnquiry => "RESPONSE_TO_STATUS_ENQUIRY",
502            Self::NormalUnspecified => "NORMAL_UNSPECIFIED",
503            Self::NormalCircuitCongestion => "NORMAL_CIRCUIT_CONGESTION",
504            Self::NetworkOutOfOrder => "NETWORK_OUT_OF_ORDER",
505            Self::NormalTemporaryFailure => "NORMAL_TEMPORARY_FAILURE",
506            Self::SwitchCongestion => "SWITCH_CONGESTION",
507            Self::AccessInfoDiscarded => "ACCESS_INFO_DISCARDED",
508            Self::RequestedChanUnavail => "REQUESTED_CHAN_UNAVAIL",
509            Self::PreEmpted => "PRE_EMPTED",
510            Self::FacilityNotSubscribed => "FACILITY_NOT_SUBSCRIBED",
511            Self::OutgoingCallBarred => "OUTGOING_CALL_BARRED",
512            Self::IncomingCallBarred => "INCOMING_CALL_BARRED",
513            Self::BearercapabilityNotauth => "BEARERCAPABILITY_NOTAUTH",
514            Self::BearercapabilityNotavail => "BEARERCAPABILITY_NOTAVAIL",
515            Self::ServiceUnavailable => "SERVICE_UNAVAILABLE",
516            Self::BearercapabilityNotimpl => "BEARERCAPABILITY_NOTIMPL",
517            Self::ChanNotImplemented => "CHAN_NOT_IMPLEMENTED",
518            Self::FacilityNotImplemented => "FACILITY_NOT_IMPLEMENTED",
519            Self::ServiceNotImplemented => "SERVICE_NOT_IMPLEMENTED",
520            Self::InvalidCallReference => "INVALID_CALL_REFERENCE",
521            Self::IncompatibleDestination => "INCOMPATIBLE_DESTINATION",
522            Self::InvalidMsgUnspecified => "INVALID_MSG_UNSPECIFIED",
523            Self::MandatoryIeMissing => "MANDATORY_IE_MISSING",
524            Self::MessageTypeNonexist => "MESSAGE_TYPE_NONEXIST",
525            Self::WrongMessage => "WRONG_MESSAGE",
526            Self::IeNonexist => "IE_NONEXIST",
527            Self::InvalidIeContents => "INVALID_IE_CONTENTS",
528            Self::WrongCallState => "WRONG_CALL_STATE",
529            Self::RecoveryOnTimerExpire => "RECOVERY_ON_TIMER_EXPIRE",
530            Self::MandatoryIeLengthError => "MANDATORY_IE_LENGTH_ERROR",
531            Self::ProtocolError => "PROTOCOL_ERROR",
532            Self::Interworking => "INTERWORKING",
533            Self::Success => "SUCCESS",
534            Self::OriginatorCancel => "ORIGINATOR_CANCEL",
535            Self::Crash => "CRASH",
536            Self::SystemShutdown => "SYSTEM_SHUTDOWN",
537            Self::LoseRace => "LOSE_RACE",
538            Self::ManagerRequest => "MANAGER_REQUEST",
539            Self::BlindTransfer => "BLIND_TRANSFER",
540            Self::AttendedTransfer => "ATTENDED_TRANSFER",
541            Self::AllottedTimeout => "ALLOTTED_TIMEOUT",
542            Self::UserChallenge => "USER_CHALLENGE",
543            Self::MediaTimeout => "MEDIA_TIMEOUT",
544            Self::PickedOff => "PICKED_OFF",
545            Self::UserNotRegistered => "USER_NOT_REGISTERED",
546            Self::ProgressTimeout => "PROGRESS_TIMEOUT",
547            Self::InvalidGateway => "INVALID_GATEWAY",
548            Self::GatewayDown => "GATEWAY_DOWN",
549            Self::InvalidUrl => "INVALID_URL",
550            Self::InvalidProfile => "INVALID_PROFILE",
551            Self::NoPickup => "NO_PICKUP",
552            Self::SrtpReadError => "SRTP_READ_ERROR",
553            Self::Bowout => "BOWOUT",
554            Self::BusyEverywhere => "BUSY_EVERYWHERE",
555            Self::Decline => "DECLINE",
556            Self::DoesNotExistAnywhere => "DOES_NOT_EXIST_ANYWHERE",
557            Self::NotAcceptable => "NOT_ACCEPTABLE",
558            Self::Unwanted => "UNWANTED",
559            Self::NoIdentity => "NO_IDENTITY",
560            Self::BadIdentityInfo => "BAD_IDENTITY_INFO",
561            Self::UnsupportedCertificate => "UNSUPPORTED_CERTIFICATE",
562            Self::InvalidIdentity => "INVALID_IDENTITY",
563            Self::StaleDate => "STALE_DATE",
564            Self::RejectAll => "REJECT_ALL",
565        };
566        f.write_str(name)
567    }
568}
569
570impl FromStr for HangupCause {
571    type Err = ParseHangupCauseError;
572
573    fn from_str(s: &str) -> Result<Self, Self::Err> {
574        Ok(match s {
575            "NONE" => Self::None,
576            "UNALLOCATED_NUMBER" => Self::UnallocatedNumber,
577            "NO_ROUTE_TRANSIT_NET" => Self::NoRouteTransitNet,
578            "NO_ROUTE_DESTINATION" => Self::NoRouteDestination,
579            "CHANNEL_UNACCEPTABLE" => Self::ChannelUnacceptable,
580            "CALL_AWARDED_DELIVERED" => Self::CallAwardedDelivered,
581            "NORMAL_CLEARING" => Self::NormalClearing,
582            "USER_BUSY" => Self::UserBusy,
583            "NO_USER_RESPONSE" => Self::NoUserResponse,
584            "NO_ANSWER" => Self::NoAnswer,
585            "SUBSCRIBER_ABSENT" => Self::SubscriberAbsent,
586            "CALL_REJECTED" => Self::CallRejected,
587            "NUMBER_CHANGED" => Self::NumberChanged,
588            "REDIRECTION_TO_NEW_DESTINATION" => Self::RedirectionToNewDestination,
589            "EXCHANGE_ROUTING_ERROR" => Self::ExchangeRoutingError,
590            "DESTINATION_OUT_OF_ORDER" => Self::DestinationOutOfOrder,
591            "INVALID_NUMBER_FORMAT" => Self::InvalidNumberFormat,
592            "FACILITY_REJECTED" => Self::FacilityRejected,
593            "RESPONSE_TO_STATUS_ENQUIRY" => Self::ResponseToStatusEnquiry,
594            "NORMAL_UNSPECIFIED" => Self::NormalUnspecified,
595            "NORMAL_CIRCUIT_CONGESTION" => Self::NormalCircuitCongestion,
596            "NETWORK_OUT_OF_ORDER" => Self::NetworkOutOfOrder,
597            "NORMAL_TEMPORARY_FAILURE" => Self::NormalTemporaryFailure,
598            "SWITCH_CONGESTION" => Self::SwitchCongestion,
599            "ACCESS_INFO_DISCARDED" => Self::AccessInfoDiscarded,
600            "REQUESTED_CHAN_UNAVAIL" => Self::RequestedChanUnavail,
601            "PRE_EMPTED" => Self::PreEmpted,
602            "FACILITY_NOT_SUBSCRIBED" => Self::FacilityNotSubscribed,
603            "OUTGOING_CALL_BARRED" => Self::OutgoingCallBarred,
604            "INCOMING_CALL_BARRED" => Self::IncomingCallBarred,
605            "BEARERCAPABILITY_NOTAUTH" => Self::BearercapabilityNotauth,
606            "BEARERCAPABILITY_NOTAVAIL" => Self::BearercapabilityNotavail,
607            "SERVICE_UNAVAILABLE" => Self::ServiceUnavailable,
608            "BEARERCAPABILITY_NOTIMPL" => Self::BearercapabilityNotimpl,
609            "CHAN_NOT_IMPLEMENTED" => Self::ChanNotImplemented,
610            "FACILITY_NOT_IMPLEMENTED" => Self::FacilityNotImplemented,
611            "SERVICE_NOT_IMPLEMENTED" => Self::ServiceNotImplemented,
612            "INVALID_CALL_REFERENCE" => Self::InvalidCallReference,
613            "INCOMPATIBLE_DESTINATION" => Self::IncompatibleDestination,
614            "INVALID_MSG_UNSPECIFIED" => Self::InvalidMsgUnspecified,
615            "MANDATORY_IE_MISSING" => Self::MandatoryIeMissing,
616            "MESSAGE_TYPE_NONEXIST" => Self::MessageTypeNonexist,
617            "WRONG_MESSAGE" => Self::WrongMessage,
618            "IE_NONEXIST" => Self::IeNonexist,
619            "INVALID_IE_CONTENTS" => Self::InvalidIeContents,
620            "WRONG_CALL_STATE" => Self::WrongCallState,
621            "RECOVERY_ON_TIMER_EXPIRE" => Self::RecoveryOnTimerExpire,
622            "MANDATORY_IE_LENGTH_ERROR" => Self::MandatoryIeLengthError,
623            "PROTOCOL_ERROR" => Self::ProtocolError,
624            "INTERWORKING" => Self::Interworking,
625            "SUCCESS" => Self::Success,
626            "ORIGINATOR_CANCEL" => Self::OriginatorCancel,
627            "CRASH" => Self::Crash,
628            "SYSTEM_SHUTDOWN" => Self::SystemShutdown,
629            "LOSE_RACE" => Self::LoseRace,
630            "MANAGER_REQUEST" => Self::ManagerRequest,
631            "BLIND_TRANSFER" => Self::BlindTransfer,
632            "ATTENDED_TRANSFER" => Self::AttendedTransfer,
633            "ALLOTTED_TIMEOUT" => Self::AllottedTimeout,
634            "USER_CHALLENGE" => Self::UserChallenge,
635            "MEDIA_TIMEOUT" => Self::MediaTimeout,
636            "PICKED_OFF" => Self::PickedOff,
637            "USER_NOT_REGISTERED" => Self::UserNotRegistered,
638            "PROGRESS_TIMEOUT" => Self::ProgressTimeout,
639            "INVALID_GATEWAY" => Self::InvalidGateway,
640            "GATEWAY_DOWN" => Self::GatewayDown,
641            "INVALID_URL" => Self::InvalidUrl,
642            "INVALID_PROFILE" => Self::InvalidProfile,
643            "NO_PICKUP" => Self::NoPickup,
644            "SRTP_READ_ERROR" => Self::SrtpReadError,
645            "BOWOUT" => Self::Bowout,
646            "BUSY_EVERYWHERE" => Self::BusyEverywhere,
647            "DECLINE" => Self::Decline,
648            "DOES_NOT_EXIST_ANYWHERE" => Self::DoesNotExistAnywhere,
649            "NOT_ACCEPTABLE" => Self::NotAcceptable,
650            "UNWANTED" => Self::Unwanted,
651            "NO_IDENTITY" => Self::NoIdentity,
652            "BAD_IDENTITY_INFO" => Self::BadIdentityInfo,
653            "UNSUPPORTED_CERTIFICATE" => Self::UnsupportedCertificate,
654            "INVALID_IDENTITY" => Self::InvalidIdentity,
655            "STALE_DATE" => Self::StaleDate,
656            "REJECT_ALL" => Self::RejectAll,
657            _ => return Err(ParseHangupCauseError(s.to_string())),
658        })
659    }
660}
661
662/// Channel timing data from FreeSWITCH's `switch_channel_timetable_t`.
663///
664/// Timestamps are epoch microseconds (`i64`). A value of `0` means the
665/// corresponding event never occurred (e.g., `hungup == Some(0)` means
666/// the channel has not hung up yet). `None` means the header was absent
667/// or unparseable.
668///
669/// Extracted from ESL event headers using a prefix (typically `"Caller"`
670/// or `"Other-Leg"`). The wire header format is `{prefix}-{suffix}`.
671///
672/// ## Headers extracted
673///
674/// `from_lookup()` extracts headers with suffixes from [`SUFFIXES`](Self::SUFFIXES).
675/// With `TimetablePrefix::Caller`, extracts `Caller-Channel-Created-Time` →
676/// `created`, `Caller-Channel-Hangup-Time` → `hungup`, etc.
677///
678/// Use `TimetablePrefix::OtherLeg` for `Other-Leg-*` headers,
679/// `TimetablePrefix::Channel` for outbound ESL `Channel-*` headers, or pass
680/// a custom string prefix to `from_lookup()`. See [`SUFFIXES`](Self::SUFFIXES)
681/// for the complete list.
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
683#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
684#[non_exhaustive]
685pub struct ChannelTimetable {
686    /// When the caller profile was created.
687    pub profile_created: Option<i64>,
688    /// When the channel was created.
689    pub created: Option<i64>,
690    /// When the channel was answered.
691    pub answered: Option<i64>,
692    /// When early media (183) was received.
693    pub progress: Option<i64>,
694    /// When media-bearing early media arrived.
695    pub progress_media: Option<i64>,
696    /// When the channel hung up.
697    pub hungup: Option<i64>,
698    /// When the channel was transferred.
699    pub transferred: Option<i64>,
700    /// When the channel was resurrected.
701    pub resurrected: Option<i64>,
702    /// When the channel was bridged.
703    pub bridged: Option<i64>,
704    /// Timestamp of the last hold event.
705    pub last_hold: Option<i64>,
706    /// Accumulated hold time in microseconds.
707    pub hold_accum: Option<i64>,
708}
709
710/// Header prefix identifying which call leg's timetable to extract.
711///
712/// FreeSWITCH emits timetable headers as `{prefix}-Channel-Created-Time`, etc.
713/// The prefix varies by context -- `Caller` for the primary leg, `Other-Leg`
714/// for the bridged party, `Channel` in outbound ESL mode, etc.
715#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
716#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
717#[non_exhaustive]
718pub enum TimetablePrefix {
719    /// Primary call leg (`Caller-*`).
720    Caller,
721    /// Bridged party (`Other-Leg-*`).
722    OtherLeg,
723    /// Outbound ESL channel profile (`Channel-*`).
724    Channel,
725    /// XML dialplan hunt (`Hunt-*`).
726    Hunt,
727    /// Bridge debug originator (`ORIGINATOR-*`).
728    Originator,
729    /// Bridge debug originatee (`ORIGINATEE-*`).
730    Originatee,
731    /// Post-bridge debug originator (`POST-ORIGINATOR-*`).
732    PostOriginator,
733    /// Post-bridge debug originatee (`POST-ORIGINATEE-*`).
734    PostOriginatee,
735}
736
737impl TimetablePrefix {
738    /// Wire-format prefix string.
739    pub fn as_str(&self) -> &'static str {
740        match self {
741            Self::Caller => "Caller",
742            Self::OtherLeg => "Other-Leg",
743            Self::Channel => "Channel",
744            Self::Hunt => "Hunt",
745            Self::Originator => "ORIGINATOR",
746            Self::Originatee => "ORIGINATEE",
747            Self::PostOriginator => "POST-ORIGINATOR",
748            Self::PostOriginatee => "POST-ORIGINATEE",
749        }
750    }
751}
752
753impl fmt::Display for TimetablePrefix {
754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755        f.write_str(self.as_str())
756    }
757}
758
759impl AsRef<str> for TimetablePrefix {
760    fn as_ref(&self) -> &str {
761        self.as_str()
762    }
763}
764
765/// Error returned when a timetable header is present but not a valid `i64`.
766#[derive(Debug, Clone, PartialEq, Eq)]
767#[non_exhaustive]
768pub struct ParseTimetableError {
769    /// Full header name (e.g. `Caller-Channel-Created-Time`).
770    pub header: String,
771    /// The unparseable value found in the header.
772    pub value: String,
773}
774
775impl fmt::Display for ParseTimetableError {
776    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
777        write!(
778            f,
779            "invalid timetable value for {}: {:?}",
780            self.header, self.value
781        )
782    }
783}
784
785impl std::error::Error for ParseTimetableError {}
786
787impl ParseTimetableError {
788    /// Create a new timetable parse error.
789    pub fn new(header: impl Into<String>, value: impl Into<String>) -> Self {
790        Self {
791            header: header.into(),
792            value: value.into(),
793        }
794    }
795}
796
797impl ChannelTimetable {
798    /// Header suffixes extracted by `from_lookup()`.
799    ///
800    /// Combine with a `TimetablePrefix` to build full header names for event
801    /// subscriptions or filters:
802    ///
803    /// ```
804    /// use freeswitch_types::{ChannelTimetable, TimetablePrefix};
805    ///
806    /// let prefix = TimetablePrefix::Caller.as_str();
807    /// let headers: Vec<String> = ChannelTimetable::SUFFIXES
808    ///     .iter()
809    ///     .map(|suffix| format!("{prefix}-{suffix}"))
810    ///     .collect();
811    /// assert!(headers.contains(&"Caller-Channel-Created-Time".to_string()));
812    /// ```
813    pub const SUFFIXES: &'static [&'static str] = &[
814        "Profile-Created-Time",
815        "Channel-Created-Time",
816        "Channel-Answered-Time",
817        "Channel-Progress-Time",
818        "Channel-Progress-Media-Time",
819        "Channel-Hangup-Time",
820        "Channel-Transfer-Time",
821        "Channel-Resurrect-Time",
822        "Channel-Bridged-Time",
823        "Channel-Last-Hold",
824        "Channel-Hold-Accum",
825    ];
826
827    /// Extract a timetable by looking up prefixed header names via a closure.
828    ///
829    /// The closure receives full header names (e.g. `"Caller-Channel-Created-Time"`)
830    /// and should return the raw value if present. Works with any key-value store:
831    /// `HashMap<String, String>`, `EslEvent`, `BTreeMap`, etc.
832    ///
833    /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
834    /// Returns `Err` if a header is present but contains an invalid (non-`i64`) value.
835    ///
836    /// ```
837    /// use std::collections::HashMap;
838    /// use freeswitch_types::{ChannelTimetable, TimetablePrefix};
839    ///
840    /// let mut headers: HashMap<String, String> = HashMap::new();
841    /// headers.insert("Caller-Channel-Created-Time".into(), "1700000001000000".into());
842    ///
843    /// // With enum:
844    /// let tt = ChannelTimetable::from_lookup(TimetablePrefix::Caller, |k| headers.get(k).map(|v: &String| v.as_str()));
845    /// assert!(tt.unwrap().unwrap().created.is_some());
846    ///
847    /// // With raw string (e.g. for dynamic "Call-1" prefix):
848    /// let tt = ChannelTimetable::from_lookup("Caller", |k| headers.get(k).map(|v: &String| v.as_str()));
849    /// assert!(tt.unwrap().unwrap().created.is_some());
850    /// ```
851    pub fn from_lookup<'a>(
852        prefix: impl AsRef<str>,
853        lookup: impl Fn(&str) -> Option<&'a str>,
854    ) -> Result<Option<Self>, ParseTimetableError> {
855        let prefix = prefix.as_ref();
856        let mut tt = Self::default();
857        let mut found = false;
858
859        macro_rules! field {
860            ($field:ident, $suffix:literal) => {
861                let header = format!("{}-{}", prefix, $suffix);
862                if let Some(raw) = lookup(&header) {
863                    let v: i64 = raw
864                        .parse()
865                        .map_err(|_| ParseTimetableError {
866                            header: header.clone(),
867                            value: raw.to_string(),
868                        })?;
869                    tt.$field = Some(v);
870                    found = true;
871                }
872            };
873        }
874
875        field!(profile_created, "Profile-Created-Time");
876        field!(created, "Channel-Created-Time");
877        field!(answered, "Channel-Answered-Time");
878        field!(progress, "Channel-Progress-Time");
879        field!(progress_media, "Channel-Progress-Media-Time");
880        field!(hungup, "Channel-Hangup-Time");
881        field!(transferred, "Channel-Transfer-Time");
882        field!(resurrected, "Channel-Resurrect-Time");
883        field!(bridged, "Channel-Bridged-Time");
884        field!(last_hold, "Channel-Last-Hold");
885        field!(hold_accum, "Channel-Hold-Accum");
886
887        if found {
888            Ok(Some(tt))
889        } else {
890            Ok(None)
891        }
892    }
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898    use crate::event::EslEvent;
899    use crate::lookup::HeaderLookup;
900
901    // --- ChannelState tests ---
902
903    #[test]
904    fn test_channel_state_display() {
905        assert_eq!(ChannelState::CsNew.to_string(), "CS_NEW");
906        assert_eq!(ChannelState::CsInit.to_string(), "CS_INIT");
907        assert_eq!(ChannelState::CsRouting.to_string(), "CS_ROUTING");
908        assert_eq!(ChannelState::CsSoftExecute.to_string(), "CS_SOFT_EXECUTE");
909        assert_eq!(ChannelState::CsExecute.to_string(), "CS_EXECUTE");
910        assert_eq!(
911            ChannelState::CsExchangeMedia.to_string(),
912            "CS_EXCHANGE_MEDIA"
913        );
914        assert_eq!(ChannelState::CsPark.to_string(), "CS_PARK");
915        assert_eq!(ChannelState::CsConsumeMedia.to_string(), "CS_CONSUME_MEDIA");
916        assert_eq!(ChannelState::CsHibernate.to_string(), "CS_HIBERNATE");
917        assert_eq!(ChannelState::CsReset.to_string(), "CS_RESET");
918        assert_eq!(ChannelState::CsHangup.to_string(), "CS_HANGUP");
919        assert_eq!(ChannelState::CsReporting.to_string(), "CS_REPORTING");
920        assert_eq!(ChannelState::CsDestroy.to_string(), "CS_DESTROY");
921        assert_eq!(ChannelState::CsNone.to_string(), "CS_NONE");
922    }
923
924    #[test]
925    fn test_channel_state_from_str() {
926        assert_eq!("CS_NEW".parse::<ChannelState>(), Ok(ChannelState::CsNew));
927        assert_eq!(
928            "CS_EXECUTE".parse::<ChannelState>(),
929            Ok(ChannelState::CsExecute)
930        );
931        assert_eq!(
932            "CS_HANGUP".parse::<ChannelState>(),
933            Ok(ChannelState::CsHangup)
934        );
935        assert_eq!(
936            "CS_DESTROY".parse::<ChannelState>(),
937            Ok(ChannelState::CsDestroy)
938        );
939    }
940
941    #[test]
942    fn test_channel_state_from_str_rejects_wrong_case() {
943        assert!("cs_new"
944            .parse::<ChannelState>()
945            .is_err());
946        assert!("Cs_Routing"
947            .parse::<ChannelState>()
948            .is_err());
949    }
950
951    #[test]
952    fn test_channel_state_from_str_unknown() {
953        assert!("CS_BOGUS"
954            .parse::<ChannelState>()
955            .is_err());
956        assert!(""
957            .parse::<ChannelState>()
958            .is_err());
959    }
960
961    #[test]
962    fn test_channel_state_from_number() {
963        assert_eq!(ChannelState::from_number(0), Some(ChannelState::CsNew));
964        assert_eq!(ChannelState::from_number(4), Some(ChannelState::CsExecute));
965        assert_eq!(ChannelState::from_number(10), Some(ChannelState::CsHangup));
966        assert_eq!(ChannelState::from_number(13), Some(ChannelState::CsNone));
967        assert_eq!(ChannelState::from_number(14), None);
968        assert_eq!(ChannelState::from_number(255), None);
969    }
970
971    #[test]
972    fn test_channel_state_as_number() {
973        assert_eq!(ChannelState::CsNew.as_number(), 0);
974        assert_eq!(ChannelState::CsExecute.as_number(), 4);
975        assert_eq!(ChannelState::CsHangup.as_number(), 10);
976        assert_eq!(ChannelState::CsNone.as_number(), 13);
977    }
978
979    #[test]
980    fn channel_state_ordering_follows_lifecycle() {
981        assert!(ChannelState::CsNew < ChannelState::CsInit);
982        assert!(ChannelState::CsInit < ChannelState::CsRouting);
983        assert!(ChannelState::CsRouting < ChannelState::CsExecute);
984        assert!(ChannelState::CsExecute < ChannelState::CsHangup);
985        assert!(ChannelState::CsHangup < ChannelState::CsReporting);
986        assert!(ChannelState::CsReporting < ChannelState::CsDestroy);
987    }
988
989    // Negated >= reads as "not yet in teardown", which is the real consumer intent.
990    #[allow(clippy::nonminimal_bool)]
991    #[test]
992    fn channel_state_teardown_check() {
993        assert!(ChannelState::CsHangup >= ChannelState::CsHangup);
994        assert!(ChannelState::CsReporting >= ChannelState::CsHangup);
995        assert!(ChannelState::CsDestroy >= ChannelState::CsHangup);
996        assert!(!(ChannelState::CsExecute >= ChannelState::CsHangup));
997        assert!(!(ChannelState::CsPark >= ChannelState::CsHangup));
998    }
999
1000    // --- CallState tests ---
1001
1002    #[test]
1003    fn call_state_ordering_matches_c_enum() {
1004        assert!(CallState::Down < CallState::Dialing);
1005        assert!(CallState::Dialing < CallState::Ringing);
1006        assert!(CallState::Early < CallState::Active);
1007        assert!(CallState::Active < CallState::Hangup);
1008    }
1009
1010    #[test]
1011    fn test_call_state_display() {
1012        assert_eq!(CallState::Down.to_string(), "DOWN");
1013        assert_eq!(CallState::Dialing.to_string(), "DIALING");
1014        assert_eq!(CallState::Ringing.to_string(), "RINGING");
1015        assert_eq!(CallState::Early.to_string(), "EARLY");
1016        assert_eq!(CallState::Active.to_string(), "ACTIVE");
1017        assert_eq!(CallState::Held.to_string(), "HELD");
1018        assert_eq!(CallState::RingWait.to_string(), "RING_WAIT");
1019        assert_eq!(CallState::Hangup.to_string(), "HANGUP");
1020        assert_eq!(CallState::Unheld.to_string(), "UNHELD");
1021    }
1022
1023    #[test]
1024    fn test_call_state_from_str() {
1025        assert_eq!("DOWN".parse::<CallState>(), Ok(CallState::Down));
1026        assert_eq!("ACTIVE".parse::<CallState>(), Ok(CallState::Active));
1027        assert_eq!("RING_WAIT".parse::<CallState>(), Ok(CallState::RingWait));
1028        assert_eq!("UNHELD".parse::<CallState>(), Ok(CallState::Unheld));
1029    }
1030
1031    #[test]
1032    fn test_call_state_from_str_rejects_wrong_case() {
1033        assert!("down"
1034            .parse::<CallState>()
1035            .is_err());
1036        assert!("Active"
1037            .parse::<CallState>()
1038            .is_err());
1039    }
1040
1041    #[test]
1042    fn test_call_state_from_str_unknown() {
1043        assert!("BOGUS"
1044            .parse::<CallState>()
1045            .is_err());
1046    }
1047
1048    // --- AnswerState tests ---
1049
1050    #[test]
1051    fn test_answer_state_display() {
1052        assert_eq!(AnswerState::Hangup.to_string(), "hangup");
1053        assert_eq!(AnswerState::Answered.to_string(), "answered");
1054        assert_eq!(AnswerState::Early.to_string(), "early");
1055        assert_eq!(AnswerState::Ringing.to_string(), "ringing");
1056    }
1057
1058    #[test]
1059    fn test_answer_state_from_str() {
1060        assert_eq!("hangup".parse::<AnswerState>(), Ok(AnswerState::Hangup));
1061        assert_eq!("answered".parse::<AnswerState>(), Ok(AnswerState::Answered));
1062        assert_eq!("early".parse::<AnswerState>(), Ok(AnswerState::Early));
1063        assert_eq!("ringing".parse::<AnswerState>(), Ok(AnswerState::Ringing));
1064    }
1065
1066    #[test]
1067    fn test_answer_state_from_str_rejects_wrong_case() {
1068        assert!("HANGUP"
1069            .parse::<AnswerState>()
1070            .is_err());
1071        assert!("Answered"
1072            .parse::<AnswerState>()
1073            .is_err());
1074    }
1075
1076    #[test]
1077    fn test_answer_state_from_str_unknown() {
1078        assert!("bogus"
1079            .parse::<AnswerState>()
1080            .is_err());
1081    }
1082
1083    // --- CallDirection tests ---
1084
1085    #[test]
1086    fn test_call_direction_display() {
1087        assert_eq!(CallDirection::Inbound.to_string(), "inbound");
1088        assert_eq!(CallDirection::Outbound.to_string(), "outbound");
1089    }
1090
1091    #[test]
1092    fn test_call_direction_from_str() {
1093        assert_eq!(
1094            "inbound".parse::<CallDirection>(),
1095            Ok(CallDirection::Inbound)
1096        );
1097        assert_eq!(
1098            "outbound".parse::<CallDirection>(),
1099            Ok(CallDirection::Outbound)
1100        );
1101    }
1102
1103    #[test]
1104    fn test_call_direction_from_str_rejects_wrong_case() {
1105        assert!("INBOUND"
1106            .parse::<CallDirection>()
1107            .is_err());
1108        assert!("Outbound"
1109            .parse::<CallDirection>()
1110            .is_err());
1111    }
1112
1113    #[test]
1114    fn test_call_direction_from_str_unknown() {
1115        assert!("bogus"
1116            .parse::<CallDirection>()
1117            .is_err());
1118    }
1119
1120    // --- HangupCause tests ---
1121
1122    #[test]
1123    fn hangup_cause_display() {
1124        assert_eq!(HangupCause::NormalClearing.to_string(), "NORMAL_CLEARING");
1125        assert_eq!(HangupCause::UserBusy.to_string(), "USER_BUSY");
1126        assert_eq!(
1127            HangupCause::OriginatorCancel.to_string(),
1128            "ORIGINATOR_CANCEL"
1129        );
1130        assert_eq!(HangupCause::None.to_string(), "NONE");
1131    }
1132
1133    #[test]
1134    fn hangup_cause_from_str() {
1135        assert_eq!(
1136            "NORMAL_CLEARING"
1137                .parse::<HangupCause>()
1138                .unwrap(),
1139            HangupCause::NormalClearing
1140        );
1141        assert_eq!(
1142            "USER_BUSY"
1143                .parse::<HangupCause>()
1144                .unwrap(),
1145            HangupCause::UserBusy
1146        );
1147    }
1148
1149    #[test]
1150    fn hangup_cause_from_str_rejects_wrong_case() {
1151        assert!("normal_clearing"
1152            .parse::<HangupCause>()
1153            .is_err());
1154        assert!("User_Busy"
1155            .parse::<HangupCause>()
1156            .is_err());
1157    }
1158
1159    #[test]
1160    fn hangup_cause_from_str_unknown() {
1161        assert!("BOGUS_CAUSE"
1162            .parse::<HangupCause>()
1163            .is_err());
1164    }
1165
1166    #[test]
1167    fn hangup_cause_display_round_trip() {
1168        let causes = [
1169            HangupCause::None,
1170            HangupCause::NormalClearing,
1171            HangupCause::UserBusy,
1172            HangupCause::NoAnswer,
1173            HangupCause::OriginatorCancel,
1174            HangupCause::BlindTransfer,
1175            HangupCause::InvalidIdentity,
1176        ];
1177        for cause in causes {
1178            let s = cause.to_string();
1179            let parsed: HangupCause = s
1180                .parse()
1181                .unwrap();
1182            assert_eq!(parsed, cause);
1183        }
1184    }
1185
1186    #[test]
1187    fn hangup_cause_as_number_q850() {
1188        assert_eq!(HangupCause::None.as_number(), 0);
1189        assert_eq!(HangupCause::UnallocatedNumber.as_number(), 1);
1190        assert_eq!(HangupCause::NormalClearing.as_number(), 16);
1191        assert_eq!(HangupCause::UserBusy.as_number(), 17);
1192        assert_eq!(HangupCause::NoAnswer.as_number(), 19);
1193        assert_eq!(HangupCause::CallRejected.as_number(), 21);
1194        assert_eq!(HangupCause::NormalUnspecified.as_number(), 31);
1195        assert_eq!(HangupCause::Interworking.as_number(), 127);
1196    }
1197
1198    #[test]
1199    fn hangup_cause_as_number_freeswitch_extensions() {
1200        assert_eq!(HangupCause::Success.as_number(), 142);
1201        assert_eq!(HangupCause::OriginatorCancel.as_number(), 487);
1202        assert_eq!(HangupCause::Crash.as_number(), 700);
1203        assert_eq!(HangupCause::BlindTransfer.as_number(), 800);
1204        assert_eq!(HangupCause::InvalidIdentity.as_number(), 823);
1205    }
1206
1207    #[test]
1208    fn hangup_cause_from_number_round_trip() {
1209        let codes: &[u16] = &[0, 1, 16, 17, 19, 21, 31, 127, 142, 487, 700, 800, 823];
1210        for &code in codes {
1211            let cause = HangupCause::from_number(code).unwrap();
1212            assert_eq!(cause.as_number(), code);
1213        }
1214    }
1215
1216    #[test]
1217    fn hangup_cause_from_number_unknown() {
1218        assert!(HangupCause::from_number(999).is_none());
1219        assert!(HangupCause::from_number(4).is_none());
1220    }
1221
1222    // --- ChannelTimetable tests ---
1223
1224    #[test]
1225    fn caller_timetable_all_fields() {
1226        let mut event = EslEvent::new();
1227        event.set_header("Caller-Profile-Created-Time", "1700000000000000");
1228        event.set_header("Caller-Channel-Created-Time", "1700000001000000");
1229        event.set_header("Caller-Channel-Answered-Time", "1700000005000000");
1230        event.set_header("Caller-Channel-Progress-Time", "1700000002000000");
1231        event.set_header("Caller-Channel-Progress-Media-Time", "1700000003000000");
1232        event.set_header("Caller-Channel-Hangup-Time", "0");
1233        event.set_header("Caller-Channel-Transfer-Time", "0");
1234        event.set_header("Caller-Channel-Resurrect-Time", "0");
1235        event.set_header("Caller-Channel-Bridged-Time", "1700000006000000");
1236        event.set_header("Caller-Channel-Last-Hold", "0");
1237        event.set_header("Caller-Channel-Hold-Accum", "0");
1238
1239        let tt = event
1240            .caller_timetable()
1241            .unwrap()
1242            .expect("should have timetable");
1243        assert_eq!(tt.profile_created, Some(1700000000000000));
1244        assert_eq!(tt.created, Some(1700000001000000));
1245        assert_eq!(tt.answered, Some(1700000005000000));
1246        assert_eq!(tt.progress, Some(1700000002000000));
1247        assert_eq!(tt.progress_media, Some(1700000003000000));
1248        assert_eq!(tt.hungup, Some(0));
1249        assert_eq!(tt.transferred, Some(0));
1250        assert_eq!(tt.resurrected, Some(0));
1251        assert_eq!(tt.bridged, Some(1700000006000000));
1252        assert_eq!(tt.last_hold, Some(0));
1253        assert_eq!(tt.hold_accum, Some(0));
1254    }
1255
1256    #[test]
1257    fn other_leg_timetable() {
1258        let mut event = EslEvent::new();
1259        event.set_header("Other-Leg-Profile-Created-Time", "1700000000000000");
1260        event.set_header("Other-Leg-Channel-Created-Time", "1700000001000000");
1261        event.set_header("Other-Leg-Channel-Answered-Time", "1700000005000000");
1262        event.set_header("Other-Leg-Channel-Progress-Time", "0");
1263        event.set_header("Other-Leg-Channel-Progress-Media-Time", "0");
1264        event.set_header("Other-Leg-Channel-Hangup-Time", "0");
1265        event.set_header("Other-Leg-Channel-Transfer-Time", "0");
1266        event.set_header("Other-Leg-Channel-Resurrect-Time", "0");
1267        event.set_header("Other-Leg-Channel-Bridged-Time", "1700000006000000");
1268        event.set_header("Other-Leg-Channel-Last-Hold", "0");
1269        event.set_header("Other-Leg-Channel-Hold-Accum", "0");
1270
1271        let tt = event
1272            .other_leg_timetable()
1273            .unwrap()
1274            .expect("should have timetable");
1275        assert_eq!(tt.created, Some(1700000001000000));
1276        assert_eq!(tt.bridged, Some(1700000006000000));
1277    }
1278
1279    #[test]
1280    fn timetable_no_headers() {
1281        let event = EslEvent::new();
1282        assert_eq!(
1283            event
1284                .caller_timetable()
1285                .unwrap(),
1286            None
1287        );
1288        assert_eq!(
1289            event
1290                .other_leg_timetable()
1291                .unwrap(),
1292            None
1293        );
1294    }
1295
1296    #[test]
1297    fn timetable_partial_headers() {
1298        let mut event = EslEvent::new();
1299        event.set_header("Caller-Channel-Created-Time", "1700000001000000");
1300
1301        let tt = event
1302            .caller_timetable()
1303            .unwrap()
1304            .expect("at least one field parsed");
1305        assert_eq!(tt.created, Some(1700000001000000));
1306        assert_eq!(tt.answered, None);
1307        assert_eq!(tt.profile_created, None);
1308    }
1309
1310    #[test]
1311    fn timetable_invalid_value_is_error() {
1312        let mut event = EslEvent::new();
1313        event.set_header("Caller-Channel-Created-Time", "not_a_number");
1314
1315        let err = event
1316            .caller_timetable()
1317            .unwrap_err();
1318        assert_eq!(err.header, "Caller-Channel-Created-Time");
1319        assert_eq!(err.value, "not_a_number");
1320    }
1321
1322    #[test]
1323    fn timetable_valid_then_invalid_is_error() {
1324        let mut event = EslEvent::new();
1325        event.set_header("Caller-Profile-Created-Time", "1700000000000000");
1326        event.set_header("Caller-Channel-Created-Time", "garbage");
1327
1328        let err = event
1329            .caller_timetable()
1330            .unwrap_err();
1331        assert_eq!(err.header, "Caller-Channel-Created-Time");
1332        assert_eq!(err.value, "garbage");
1333    }
1334
1335    #[test]
1336    fn timetable_zero_preserved() {
1337        let mut event = EslEvent::new();
1338        event.set_header("Caller-Channel-Hangup-Time", "0");
1339
1340        let tt = event
1341            .caller_timetable()
1342            .unwrap()
1343            .expect("should have timetable");
1344        assert_eq!(tt.hungup, Some(0));
1345    }
1346
1347    #[test]
1348    fn timetable_custom_prefix() {
1349        let mut event = EslEvent::new();
1350        event.set_header("Channel-Channel-Created-Time", "1700000001000000");
1351
1352        let tt = event
1353            .timetable("Channel")
1354            .unwrap()
1355            .expect("custom prefix should work");
1356        assert_eq!(tt.created, Some(1700000001000000));
1357    }
1358}