Skip to main content

freeswitch_types/
lookup.rs

1//! Shared trait for typed header lookups from any key-value store.
2//!
3//! [`HeaderLookup`] provides convenience accessors (typed channel state,
4//! call direction, timetable extraction, etc.) to any type that can look up
5//! headers and variables by name. Implement the two required methods and get
6//! everything else for free.
7
8use crate::channel::{
9    AnswerState, CallDirection, CallState, ChannelState, ChannelTimetable, HangupCause,
10    ParseAnswerStateError, ParseCallDirectionError, ParseCallStateError, ParseChannelStateError,
11    ParseHangupCauseError, ParseTimetableError,
12};
13#[cfg(feature = "esl")]
14use crate::event::{EslEventPriority, ParsePriorityError};
15use crate::headers::EventHeader;
16use crate::variables::VariableName;
17
18/// Trait for looking up ESL headers and channel variables from any key-value store.
19///
20/// Implementors provide two methods — `header_str(&str)` and `variable_str(&str)` —
21/// and get all typed accessors (`channel_state()`, `call_direction()`, `timetable()`,
22/// etc.) as default implementations.
23///
24/// This trait must be in scope to call its methods on `EslEvent` -- including
25/// `unique_id()`, `hangup_cause()`, and `channel_state()`. Import it directly
26/// or via the prelude:
27///
28/// ```ignore
29/// use freeswitch_esl_tokio::prelude::*;
30/// // or: use freeswitch_esl_tokio::HeaderLookup;
31/// // or: use freeswitch_types::HeaderLookup;
32/// ```
33///
34/// # Example
35///
36/// ```
37/// use std::collections::HashMap;
38/// use freeswitch_types::{HeaderLookup, EventHeader, ChannelVariable};
39///
40/// struct MyStore(HashMap<String, String>);
41///
42/// impl HeaderLookup for MyStore {
43///     fn header_str(&self, name: &str) -> Option<&str> {
44///         self.0.get(name).map(|s| s.as_str())
45///     }
46///     fn variable_str(&self, name: &str) -> Option<&str> {
47///         self.0.get(&format!("variable_{}", name)).map(|s| s.as_str())
48///     }
49/// }
50///
51/// let mut map = HashMap::new();
52/// map.insert("Channel-State".into(), "CS_EXECUTE".into());
53/// map.insert("variable_read_codec".into(), "PCMU".into());
54/// let store = MyStore(map);
55///
56/// // Typed accessor from the trait (returns Result<Option<T>, E>):
57/// assert!(store.channel_state().unwrap().is_some());
58///
59/// // Enum-based lookups:
60/// assert_eq!(store.header(EventHeader::ChannelState), Some("CS_EXECUTE"));
61/// assert_eq!(store.variable(ChannelVariable::ReadCodec), Some("PCMU"));
62/// ```
63pub trait HeaderLookup {
64    /// Look up a header by its raw wire name (e.g. `"Unique-ID"`).
65    fn header_str(&self, name: &str) -> Option<&str>;
66
67    /// Look up a channel variable by its bare name (e.g. `"sip_call_id"`).
68    ///
69    /// Implementations typically prepend `variable_` and delegate to `header_str`.
70    fn variable_str(&self, name: &str) -> Option<&str>;
71
72    /// Look up a header by its [`EventHeader`] enum variant.
73    fn header(&self, name: EventHeader) -> Option<&str> {
74        self.header_str(name.as_str())
75    }
76
77    /// Look up a channel variable by its typed enum variant.
78    fn variable(&self, name: impl VariableName) -> Option<&str> {
79        self.variable_str(name.as_str())
80    }
81
82    /// `Unique-ID` header, falling back to `Caller-Unique-ID`.
83    fn unique_id(&self) -> Option<&str> {
84        self.header(EventHeader::UniqueId)
85            .or_else(|| self.header(EventHeader::CallerUniqueId))
86    }
87
88    /// `Job-UUID` header from `bgapi` `BACKGROUND_JOB` events.
89    fn job_uuid(&self) -> Option<&str> {
90        self.header(EventHeader::JobUuid)
91    }
92
93    /// `Channel-Name` header (e.g. `sofia/internal/1000@domain`).
94    fn channel_name(&self) -> Option<&str> {
95        self.header(EventHeader::ChannelName)
96    }
97
98    /// `Caller-Caller-ID-Number` header.
99    fn caller_id_number(&self) -> Option<&str> {
100        self.header(EventHeader::CallerCallerIdNumber)
101    }
102
103    /// `Caller-Caller-ID-Name` header.
104    fn caller_id_name(&self) -> Option<&str> {
105        self.header(EventHeader::CallerCallerIdName)
106    }
107
108    /// `Caller-Destination-Number` header.
109    fn destination_number(&self) -> Option<&str> {
110        self.header(EventHeader::CallerDestinationNumber)
111    }
112
113    /// `Caller-Callee-ID-Number` header.
114    fn callee_id_number(&self) -> Option<&str> {
115        self.header(EventHeader::CallerCalleeIdNumber)
116    }
117
118    /// `Caller-Callee-ID-Name` header.
119    fn callee_id_name(&self) -> Option<&str> {
120        self.header(EventHeader::CallerCalleeIdName)
121    }
122
123    /// Parse the `Hangup-Cause` header into a [`HangupCause`].
124    ///
125    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
126    fn hangup_cause(&self) -> Result<Option<HangupCause>, ParseHangupCauseError> {
127        match self.header(EventHeader::HangupCause) {
128            Some(s) => Ok(Some(s.parse()?)),
129            None => Ok(None),
130        }
131    }
132
133    /// `Event-Subclass` header for `CUSTOM` events (e.g. `sofia::register`).
134    fn event_subclass(&self) -> Option<&str> {
135        self.header(EventHeader::EventSubclass)
136    }
137
138    /// `pl_data` header — SIP NOTIFY body content from `NOTIFY_IN` events.
139    ///
140    /// Contains the JSON payload (already percent-decoded by the ESL parser).
141    /// For NG9-1-1 events this is the inner object without the wrapper key
142    /// (FreeSWITCH strips it).
143    fn pl_data(&self) -> Option<&str> {
144        self.header(EventHeader::PlData)
145    }
146
147    /// `event` header — SIP event package name from `NOTIFY_IN` events.
148    ///
149    /// Examples: `emergency-AbandonedCall`, `emergency-ServiceState`.
150    fn sip_event(&self) -> Option<&str> {
151        self.header(EventHeader::SipEvent)
152    }
153
154    /// `gateway_name` header — gateway that received a SIP NOTIFY.
155    fn gateway_name(&self) -> Option<&str> {
156        self.header(EventHeader::GatewayName)
157    }
158
159    /// Parse the `Channel-State` header into a [`ChannelState`].
160    ///
161    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
162    fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
163        match self.header(EventHeader::ChannelState) {
164            Some(s) => Ok(Some(s.parse()?)),
165            None => Ok(None),
166        }
167    }
168
169    /// Parse the `Channel-State-Number` header into a [`ChannelState`].
170    ///
171    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
172    fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
173        match self.header(EventHeader::ChannelStateNumber) {
174            Some(s) => {
175                let n: u8 = s
176                    .parse()
177                    .map_err(|_| ParseChannelStateError(s.to_string()))?;
178                ChannelState::from_number(n)
179                    .ok_or_else(|| ParseChannelStateError(s.to_string()))
180                    .map(Some)
181            }
182            None => Ok(None),
183        }
184    }
185
186    /// Parse the `Channel-Call-State` header into a [`CallState`].
187    ///
188    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
189    fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
190        match self.header(EventHeader::ChannelCallState) {
191            Some(s) => Ok(Some(s.parse()?)),
192            None => Ok(None),
193        }
194    }
195
196    /// Parse the `Answer-State` header into an [`AnswerState`].
197    ///
198    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
199    fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
200        match self.header(EventHeader::AnswerState) {
201            Some(s) => Ok(Some(s.parse()?)),
202            None => Ok(None),
203        }
204    }
205
206    /// Parse the `Call-Direction` header into a [`CallDirection`].
207    ///
208    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
209    fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
210        match self.header(EventHeader::CallDirection) {
211            Some(s) => Ok(Some(s.parse()?)),
212            None => Ok(None),
213        }
214    }
215
216    /// Parse the `priority` header value.
217    ///
218    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
219    #[cfg(feature = "esl")]
220    fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
221        match self.header(EventHeader::Priority) {
222            Some(s) => Ok(Some(s.parse()?)),
223            None => Ok(None),
224        }
225    }
226
227    /// Extract timetable from timestamp headers with the given prefix.
228    ///
229    /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
230    /// Returns `Err` if a header is present but contains an invalid value.
231    fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
232        ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
233    }
234
235    /// Caller-leg channel timetable (`Caller-*-Time` headers).
236    fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
237        self.timetable("Caller")
238    }
239
240    /// Other-leg channel timetable (`Other-Leg-*-Time` headers).
241    fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
242        self.timetable("Other-Leg")
243    }
244}
245
246impl<T: HeaderLookup> crate::sip_header::SipHeaderLookup for T {
247    fn sip_header_str(&self, name: &str) -> Option<&str> {
248        self.header_str(name)
249    }
250}
251
252impl HeaderLookup for std::collections::HashMap<String, String> {
253    fn header_str(&self, name: &str) -> Option<&str> {
254        self.get(name)
255            .map(|s| s.as_str())
256    }
257
258    fn variable_str(&self, name: &str) -> Option<&str> {
259        self.get(&format!("variable_{name}"))
260            .map(|s| s.as_str())
261    }
262}
263
264#[cfg(feature = "esl")]
265impl HeaderLookup for indexmap::IndexMap<String, String> {
266    fn header_str(&self, name: &str) -> Option<&str> {
267        self.get(name)
268            .map(|s| s.as_str())
269    }
270
271    fn variable_str(&self, name: &str) -> Option<&str> {
272        self.get(&format!("variable_{name}"))
273            .map(|s| s.as_str())
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::variables::ChannelVariable;
281    use std::collections::HashMap;
282
283    struct TestStore(HashMap<String, String>);
284
285    impl HeaderLookup for TestStore {
286        fn header_str(&self, name: &str) -> Option<&str> {
287            self.0
288                .get(name)
289                .map(|s| s.as_str())
290        }
291        fn variable_str(&self, name: &str) -> Option<&str> {
292            self.0
293                .get(&format!("variable_{}", name))
294                .map(|s| s.as_str())
295        }
296    }
297
298    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
299        let map: HashMap<String, String> = pairs
300            .iter()
301            .map(|(k, v)| (k.to_string(), v.to_string()))
302            .collect();
303        TestStore(map)
304    }
305
306    #[test]
307    fn header_str_direct() {
308        let s = store_with(&[("Unique-ID", "abc-123")]);
309        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
310        assert_eq!(s.header_str("Missing"), None);
311    }
312
313    #[test]
314    fn header_by_enum() {
315        let s = store_with(&[("Unique-ID", "abc-123")]);
316        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
317    }
318
319    #[test]
320    fn variable_str_direct() {
321        let s = store_with(&[("variable_read_codec", "PCMU")]);
322        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
323        assert_eq!(s.variable_str("missing"), None);
324    }
325
326    #[test]
327    fn variable_by_enum() {
328        let s = store_with(&[("variable_read_codec", "PCMU")]);
329        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
330    }
331
332    #[test]
333    fn unique_id_primary() {
334        let s = store_with(&[("Unique-ID", "uuid-1")]);
335        assert_eq!(s.unique_id(), Some("uuid-1"));
336    }
337
338    #[test]
339    fn unique_id_fallback() {
340        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
341        assert_eq!(s.unique_id(), Some("uuid-2"));
342    }
343
344    #[test]
345    fn unique_id_none() {
346        let s = store_with(&[]);
347        assert_eq!(s.unique_id(), None);
348    }
349
350    #[test]
351    fn job_uuid() {
352        let s = store_with(&[("Job-UUID", "job-1")]);
353        assert_eq!(s.job_uuid(), Some("job-1"));
354    }
355
356    #[test]
357    fn channel_name() {
358        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
359        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
360    }
361
362    #[test]
363    fn caller_id_number_and_name() {
364        let s = store_with(&[
365            ("Caller-Caller-ID-Number", "1000"),
366            ("Caller-Caller-ID-Name", "Alice"),
367        ]);
368        assert_eq!(s.caller_id_number(), Some("1000"));
369        assert_eq!(s.caller_id_name(), Some("Alice"));
370    }
371
372    #[test]
373    fn hangup_cause_typed() {
374        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
375        assert_eq!(
376            s.hangup_cause()
377                .unwrap(),
378            Some(crate::channel::HangupCause::NormalClearing)
379        );
380    }
381
382    #[test]
383    fn hangup_cause_invalid_is_error() {
384        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
385        assert!(s
386            .hangup_cause()
387            .is_err());
388    }
389
390    #[test]
391    fn destination_number() {
392        let s = store_with(&[("Caller-Destination-Number", "1000")]);
393        assert_eq!(s.destination_number(), Some("1000"));
394    }
395
396    #[test]
397    fn callee_id() {
398        let s = store_with(&[
399            ("Caller-Callee-ID-Number", "2000"),
400            ("Caller-Callee-ID-Name", "Bob"),
401        ]);
402        assert_eq!(s.callee_id_number(), Some("2000"));
403        assert_eq!(s.callee_id_name(), Some("Bob"));
404    }
405
406    #[test]
407    fn event_subclass() {
408        let s = store_with(&[("Event-Subclass", "sofia::register")]);
409        assert_eq!(s.event_subclass(), Some("sofia::register"));
410    }
411
412    #[test]
413    fn channel_state_typed() {
414        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
415        assert_eq!(
416            s.channel_state()
417                .unwrap(),
418            Some(ChannelState::CsExecute)
419        );
420    }
421
422    #[test]
423    fn channel_state_number_typed() {
424        let s = store_with(&[("Channel-State-Number", "4")]);
425        assert_eq!(
426            s.channel_state_number()
427                .unwrap(),
428            Some(ChannelState::CsExecute)
429        );
430    }
431
432    #[test]
433    fn call_state_typed() {
434        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
435        assert_eq!(
436            s.call_state()
437                .unwrap(),
438            Some(CallState::Active)
439        );
440    }
441
442    #[test]
443    fn answer_state_typed() {
444        let s = store_with(&[("Answer-State", "answered")]);
445        assert_eq!(
446            s.answer_state()
447                .unwrap(),
448            Some(AnswerState::Answered)
449        );
450    }
451
452    #[test]
453    fn call_direction_typed() {
454        let s = store_with(&[("Call-Direction", "inbound")]);
455        assert_eq!(
456            s.call_direction()
457                .unwrap(),
458            Some(CallDirection::Inbound)
459        );
460    }
461
462    #[test]
463    fn priority_typed() {
464        let s = store_with(&[("priority", "HIGH")]);
465        assert_eq!(
466            s.priority()
467                .unwrap(),
468            Some(EslEventPriority::High)
469        );
470    }
471
472    #[test]
473    fn timetable_extraction() {
474        let s = store_with(&[
475            ("Caller-Channel-Created-Time", "1700000001000000"),
476            ("Caller-Channel-Answered-Time", "1700000005000000"),
477        ]);
478        let tt = s
479            .caller_timetable()
480            .unwrap()
481            .expect("should have timetable");
482        assert_eq!(tt.created, Some(1700000001000000));
483        assert_eq!(tt.answered, Some(1700000005000000));
484        assert_eq!(tt.hungup, None);
485    }
486
487    #[test]
488    fn timetable_other_leg() {
489        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
490        let tt = s
491            .other_leg_timetable()
492            .unwrap()
493            .expect("should have timetable");
494        assert_eq!(tt.created, Some(1700000001000000));
495    }
496
497    #[test]
498    fn timetable_none_when_absent() {
499        let s = store_with(&[]);
500        assert_eq!(
501            s.caller_timetable()
502                .unwrap(),
503            None
504        );
505    }
506
507    #[test]
508    fn timetable_invalid_is_error() {
509        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
510        let err = s
511            .caller_timetable()
512            .unwrap_err();
513        assert_eq!(err.header, "Caller-Channel-Created-Time");
514    }
515
516    #[test]
517    fn missing_headers_return_none() {
518        let s = store_with(&[]);
519        assert_eq!(
520            s.channel_state()
521                .unwrap(),
522            None
523        );
524        assert_eq!(
525            s.channel_state_number()
526                .unwrap(),
527            None
528        );
529        assert_eq!(
530            s.call_state()
531                .unwrap(),
532            None
533        );
534        assert_eq!(
535            s.answer_state()
536                .unwrap(),
537            None
538        );
539        assert_eq!(
540            s.call_direction()
541                .unwrap(),
542            None
543        );
544        assert_eq!(
545            s.priority()
546                .unwrap(),
547            None
548        );
549        assert_eq!(
550            s.hangup_cause()
551                .unwrap(),
552            None
553        );
554        assert_eq!(s.channel_name(), None);
555        assert_eq!(s.caller_id_number(), None);
556        assert_eq!(s.caller_id_name(), None);
557        assert_eq!(s.destination_number(), None);
558        assert_eq!(s.callee_id_number(), None);
559        assert_eq!(s.callee_id_name(), None);
560        assert_eq!(s.event_subclass(), None);
561        assert_eq!(s.job_uuid(), None);
562        assert_eq!(s.pl_data(), None);
563        assert_eq!(s.sip_event(), None);
564        assert_eq!(s.gateway_name(), None);
565    }
566
567    #[test]
568    fn notify_in_headers() {
569        let s = store_with(&[
570            ("pl_data", r#"{"invite":"INVITE ..."}"#),
571            ("event", "emergency-AbandonedCall"),
572            ("gateway_name", "ng911-bcf"),
573        ]);
574        assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
575        assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
576        assert_eq!(s.gateway_name(), Some("ng911-bcf"));
577    }
578
579    #[test]
580    fn invalid_values_return_err() {
581        let s = store_with(&[
582            ("Channel-State", "BOGUS"),
583            ("Channel-State-Number", "999"),
584            ("Channel-Call-State", "BOGUS"),
585            ("Answer-State", "bogus"),
586            ("Call-Direction", "bogus"),
587            ("priority", "BOGUS"),
588            ("Hangup-Cause", "BOGUS"),
589        ]);
590        assert!(s
591            .channel_state()
592            .is_err());
593        assert!(s
594            .channel_state_number()
595            .is_err());
596        assert!(s
597            .call_state()
598            .is_err());
599        assert!(s
600            .answer_state()
601            .is_err());
602        assert!(s
603            .call_direction()
604            .is_err());
605        assert!(s
606            .priority()
607            .is_err());
608        assert!(s
609            .hangup_cause()
610            .is_err());
611    }
612}