Skip to main content

freeswitch_types/
channel.rs

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