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 HeaderLookup for std::collections::HashMap<String, String> {
247    fn header_str(&self, name: &str) -> Option<&str> {
248        self.get(name)
249            .map(|s| s.as_str())
250    }
251
252    fn variable_str(&self, name: &str) -> Option<&str> {
253        self.get(&format!("variable_{name}"))
254            .map(|s| s.as_str())
255    }
256}
257
258#[cfg(feature = "esl")]
259impl HeaderLookup for indexmap::IndexMap<String, String> {
260    fn header_str(&self, name: &str) -> Option<&str> {
261        self.get(name)
262            .map(|s| s.as_str())
263    }
264
265    fn variable_str(&self, name: &str) -> Option<&str> {
266        self.get(&format!("variable_{name}"))
267            .map(|s| s.as_str())
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::variables::ChannelVariable;
275    use std::collections::HashMap;
276
277    struct TestStore(HashMap<String, String>);
278
279    impl HeaderLookup for TestStore {
280        fn header_str(&self, name: &str) -> Option<&str> {
281            self.0
282                .get(name)
283                .map(|s| s.as_str())
284        }
285        fn variable_str(&self, name: &str) -> Option<&str> {
286            self.0
287                .get(&format!("variable_{}", name))
288                .map(|s| s.as_str())
289        }
290    }
291
292    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
293        let map: HashMap<String, String> = pairs
294            .iter()
295            .map(|(k, v)| (k.to_string(), v.to_string()))
296            .collect();
297        TestStore(map)
298    }
299
300    #[test]
301    fn header_str_direct() {
302        let s = store_with(&[("Unique-ID", "abc-123")]);
303        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
304        assert_eq!(s.header_str("Missing"), None);
305    }
306
307    #[test]
308    fn header_by_enum() {
309        let s = store_with(&[("Unique-ID", "abc-123")]);
310        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
311    }
312
313    #[test]
314    fn variable_str_direct() {
315        let s = store_with(&[("variable_read_codec", "PCMU")]);
316        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
317        assert_eq!(s.variable_str("missing"), None);
318    }
319
320    #[test]
321    fn variable_by_enum() {
322        let s = store_with(&[("variable_read_codec", "PCMU")]);
323        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
324    }
325
326    #[test]
327    fn unique_id_primary() {
328        let s = store_with(&[("Unique-ID", "uuid-1")]);
329        assert_eq!(s.unique_id(), Some("uuid-1"));
330    }
331
332    #[test]
333    fn unique_id_fallback() {
334        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
335        assert_eq!(s.unique_id(), Some("uuid-2"));
336    }
337
338    #[test]
339    fn unique_id_none() {
340        let s = store_with(&[]);
341        assert_eq!(s.unique_id(), None);
342    }
343
344    #[test]
345    fn job_uuid() {
346        let s = store_with(&[("Job-UUID", "job-1")]);
347        assert_eq!(s.job_uuid(), Some("job-1"));
348    }
349
350    #[test]
351    fn channel_name() {
352        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
353        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
354    }
355
356    #[test]
357    fn caller_id_number_and_name() {
358        let s = store_with(&[
359            ("Caller-Caller-ID-Number", "1000"),
360            ("Caller-Caller-ID-Name", "Alice"),
361        ]);
362        assert_eq!(s.caller_id_number(), Some("1000"));
363        assert_eq!(s.caller_id_name(), Some("Alice"));
364    }
365
366    #[test]
367    fn hangup_cause_typed() {
368        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
369        assert_eq!(
370            s.hangup_cause()
371                .unwrap(),
372            Some(crate::channel::HangupCause::NormalClearing)
373        );
374    }
375
376    #[test]
377    fn hangup_cause_invalid_is_error() {
378        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
379        assert!(s
380            .hangup_cause()
381            .is_err());
382    }
383
384    #[test]
385    fn destination_number() {
386        let s = store_with(&[("Caller-Destination-Number", "1000")]);
387        assert_eq!(s.destination_number(), Some("1000"));
388    }
389
390    #[test]
391    fn callee_id() {
392        let s = store_with(&[
393            ("Caller-Callee-ID-Number", "2000"),
394            ("Caller-Callee-ID-Name", "Bob"),
395        ]);
396        assert_eq!(s.callee_id_number(), Some("2000"));
397        assert_eq!(s.callee_id_name(), Some("Bob"));
398    }
399
400    #[test]
401    fn event_subclass() {
402        let s = store_with(&[("Event-Subclass", "sofia::register")]);
403        assert_eq!(s.event_subclass(), Some("sofia::register"));
404    }
405
406    #[test]
407    fn channel_state_typed() {
408        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
409        assert_eq!(
410            s.channel_state()
411                .unwrap(),
412            Some(ChannelState::CsExecute)
413        );
414    }
415
416    #[test]
417    fn channel_state_number_typed() {
418        let s = store_with(&[("Channel-State-Number", "4")]);
419        assert_eq!(
420            s.channel_state_number()
421                .unwrap(),
422            Some(ChannelState::CsExecute)
423        );
424    }
425
426    #[test]
427    fn call_state_typed() {
428        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
429        assert_eq!(
430            s.call_state()
431                .unwrap(),
432            Some(CallState::Active)
433        );
434    }
435
436    #[test]
437    fn answer_state_typed() {
438        let s = store_with(&[("Answer-State", "answered")]);
439        assert_eq!(
440            s.answer_state()
441                .unwrap(),
442            Some(AnswerState::Answered)
443        );
444    }
445
446    #[test]
447    fn call_direction_typed() {
448        let s = store_with(&[("Call-Direction", "inbound")]);
449        assert_eq!(
450            s.call_direction()
451                .unwrap(),
452            Some(CallDirection::Inbound)
453        );
454    }
455
456    #[test]
457    fn priority_typed() {
458        let s = store_with(&[("priority", "HIGH")]);
459        assert_eq!(
460            s.priority()
461                .unwrap(),
462            Some(EslEventPriority::High)
463        );
464    }
465
466    #[test]
467    fn timetable_extraction() {
468        let s = store_with(&[
469            ("Caller-Channel-Created-Time", "1700000001000000"),
470            ("Caller-Channel-Answered-Time", "1700000005000000"),
471        ]);
472        let tt = s
473            .caller_timetable()
474            .unwrap()
475            .expect("should have timetable");
476        assert_eq!(tt.created, Some(1700000001000000));
477        assert_eq!(tt.answered, Some(1700000005000000));
478        assert_eq!(tt.hungup, None);
479    }
480
481    #[test]
482    fn timetable_other_leg() {
483        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
484        let tt = s
485            .other_leg_timetable()
486            .unwrap()
487            .expect("should have timetable");
488        assert_eq!(tt.created, Some(1700000001000000));
489    }
490
491    #[test]
492    fn timetable_none_when_absent() {
493        let s = store_with(&[]);
494        assert_eq!(
495            s.caller_timetable()
496                .unwrap(),
497            None
498        );
499    }
500
501    #[test]
502    fn timetable_invalid_is_error() {
503        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
504        let err = s
505            .caller_timetable()
506            .unwrap_err();
507        assert_eq!(err.header, "Caller-Channel-Created-Time");
508    }
509
510    #[test]
511    fn missing_headers_return_none() {
512        let s = store_with(&[]);
513        assert_eq!(
514            s.channel_state()
515                .unwrap(),
516            None
517        );
518        assert_eq!(
519            s.channel_state_number()
520                .unwrap(),
521            None
522        );
523        assert_eq!(
524            s.call_state()
525                .unwrap(),
526            None
527        );
528        assert_eq!(
529            s.answer_state()
530                .unwrap(),
531            None
532        );
533        assert_eq!(
534            s.call_direction()
535                .unwrap(),
536            None
537        );
538        assert_eq!(
539            s.priority()
540                .unwrap(),
541            None
542        );
543        assert_eq!(
544            s.hangup_cause()
545                .unwrap(),
546            None
547        );
548        assert_eq!(s.channel_name(), None);
549        assert_eq!(s.caller_id_number(), None);
550        assert_eq!(s.caller_id_name(), None);
551        assert_eq!(s.destination_number(), None);
552        assert_eq!(s.callee_id_number(), None);
553        assert_eq!(s.callee_id_name(), None);
554        assert_eq!(s.event_subclass(), None);
555        assert_eq!(s.job_uuid(), None);
556        assert_eq!(s.pl_data(), None);
557        assert_eq!(s.sip_event(), None);
558        assert_eq!(s.gateway_name(), None);
559    }
560
561    #[test]
562    fn notify_in_headers() {
563        let s = store_with(&[
564            ("pl_data", r#"{"invite":"INVITE ..."}"#),
565            ("event", "emergency-AbandonedCall"),
566            ("gateway_name", "ng911-bcf"),
567        ]);
568        assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
569        assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
570        assert_eq!(s.gateway_name(), Some("ng911-bcf"));
571    }
572
573    #[test]
574    fn invalid_values_return_err() {
575        let s = store_with(&[
576            ("Channel-State", "BOGUS"),
577            ("Channel-State-Number", "999"),
578            ("Channel-Call-State", "BOGUS"),
579            ("Answer-State", "bogus"),
580            ("Call-Direction", "bogus"),
581            ("priority", "BOGUS"),
582            ("Hangup-Cause", "BOGUS"),
583        ]);
584        assert!(s
585            .channel_state()
586            .is_err());
587        assert!(s
588            .channel_state_number()
589            .is_err());
590        assert!(s
591            .call_state()
592            .is_err());
593        assert!(s
594            .answer_state()
595            .is_err());
596        assert!(s
597            .call_direction()
598            .is_err());
599        assert!(s
600            .priority()
601            .is_err());
602        assert!(s
603            .hangup_cause()
604            .is_err());
605    }
606}