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