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