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    /// `Channel-Presence-ID` header (e.g. `1000@example.com`).
124    fn channel_presence_id(&self) -> Option<&str> {
125        self.header(EventHeader::ChannelPresenceId)
126    }
127
128    /// `Presence-Call-Direction` header, parsed into a [`CallDirection`].
129    fn presence_call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
130        match self.header(EventHeader::PresenceCallDirection) {
131            Some(s) => Ok(Some(s.parse()?)),
132            None => Ok(None),
133        }
134    }
135
136    /// `Event-Date-Timestamp` header (microseconds since epoch).
137    fn event_date_timestamp(&self) -> Option<&str> {
138        self.header(EventHeader::EventDateTimestamp)
139    }
140
141    /// `Event-Sequence` header (sequential event counter).
142    fn event_sequence(&self) -> Option<&str> {
143        self.header(EventHeader::EventSequence)
144    }
145
146    /// `DTMF-Duration` header (digit duration in milliseconds).
147    fn dtmf_duration(&self) -> Option<&str> {
148        self.header(EventHeader::DtmfDuration)
149    }
150
151    /// `DTMF-Source` header (e.g. `rtp`, `inband`).
152    fn dtmf_source(&self) -> Option<&str> {
153        self.header(EventHeader::DtmfSource)
154    }
155
156    /// Parse the `Hangup-Cause` header into a [`HangupCause`].
157    ///
158    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
159    fn hangup_cause(&self) -> Result<Option<HangupCause>, ParseHangupCauseError> {
160        match self.header(EventHeader::HangupCause) {
161            Some(s) => Ok(Some(s.parse()?)),
162            None => Ok(None),
163        }
164    }
165
166    /// `Event-Subclass` header for `CUSTOM` events (e.g. `sofia::register`).
167    fn event_subclass(&self) -> Option<&str> {
168        self.header(EventHeader::EventSubclass)
169    }
170
171    /// `pl_data` header -- SIP NOTIFY body content from `NOTIFY_IN` events.
172    ///
173    /// Contains the JSON payload (already percent-decoded by the ESL parser).
174    /// For NG9-1-1 events this is the inner object without the wrapper key
175    /// (FreeSWITCH strips it).
176    fn pl_data(&self) -> Option<&str> {
177        self.header(EventHeader::PlData)
178    }
179
180    /// `event` header -- SIP event package name from `NOTIFY_IN` events.
181    ///
182    /// Examples: `emergency-AbandonedCall`, `emergency-ServiceState`.
183    fn sip_event(&self) -> Option<&str> {
184        self.header(EventHeader::SipEvent)
185    }
186
187    /// `gateway_name` header -- gateway that received a SIP NOTIFY.
188    fn gateway_name(&self) -> Option<&str> {
189        self.header(EventHeader::GatewayName)
190    }
191
192    /// Parse the `Channel-State` header into a [`ChannelState`].
193    ///
194    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
195    fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
196        match self.header(EventHeader::ChannelState) {
197            Some(s) => Ok(Some(s.parse()?)),
198            None => Ok(None),
199        }
200    }
201
202    /// Parse the `Channel-State-Number` header into a [`ChannelState`].
203    ///
204    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
205    fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
206        match self.header(EventHeader::ChannelStateNumber) {
207            Some(s) => {
208                let n: u8 = s
209                    .parse()
210                    .map_err(|_| ParseChannelStateError(s.to_string()))?;
211                ChannelState::from_number(n)
212                    .ok_or_else(|| ParseChannelStateError(s.to_string()))
213                    .map(Some)
214            }
215            None => Ok(None),
216        }
217    }
218
219    /// Parse the `Channel-Call-State` header into a [`CallState`].
220    ///
221    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
222    fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
223        match self.header(EventHeader::ChannelCallState) {
224            Some(s) => Ok(Some(s.parse()?)),
225            None => Ok(None),
226        }
227    }
228
229    /// Parse the `Answer-State` header into an [`AnswerState`].
230    ///
231    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
232    fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
233        match self.header(EventHeader::AnswerState) {
234            Some(s) => Ok(Some(s.parse()?)),
235            None => Ok(None),
236        }
237    }
238
239    /// Parse the `Call-Direction` header into a [`CallDirection`].
240    ///
241    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
242    fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
243        match self.header(EventHeader::CallDirection) {
244            Some(s) => Ok(Some(s.parse()?)),
245            None => Ok(None),
246        }
247    }
248
249    /// Parse the `priority` header value.
250    ///
251    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
252    #[cfg(feature = "esl")]
253    fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
254        match self.header(EventHeader::Priority) {
255            Some(s) => Ok(Some(s.parse()?)),
256            None => Ok(None),
257        }
258    }
259
260    /// Extract timetable from timestamp headers with the given prefix.
261    ///
262    /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
263    /// Returns `Err` if a header is present but contains an invalid value.
264    fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
265        ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
266    }
267
268    /// Caller-leg channel timetable (`Caller-*-Time` headers).
269    fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
270        self.timetable("Caller")
271    }
272
273    /// Other-leg channel timetable (`Other-Leg-*-Time` headers).
274    fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
275        self.timetable("Other-Leg")
276    }
277}
278
279impl HeaderLookup for std::collections::HashMap<String, String> {
280    fn header_str(&self, name: &str) -> Option<&str> {
281        self.get(name)
282            .map(|s| s.as_str())
283    }
284
285    fn variable_str(&self, name: &str) -> Option<&str> {
286        self.get(&format!("variable_{name}"))
287            .map(|s| s.as_str())
288    }
289}
290
291#[cfg(feature = "esl")]
292impl HeaderLookup for indexmap::IndexMap<String, String> {
293    fn header_str(&self, name: &str) -> Option<&str> {
294        self.get(name)
295            .map(|s| s.as_str())
296    }
297
298    fn variable_str(&self, name: &str) -> Option<&str> {
299        self.get(&format!("variable_{name}"))
300            .map(|s| s.as_str())
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::variables::ChannelVariable;
308    use std::collections::HashMap;
309
310    struct TestStore(HashMap<String, String>);
311
312    impl HeaderLookup for TestStore {
313        fn header_str(&self, name: &str) -> Option<&str> {
314            self.0
315                .get(name)
316                .map(|s| s.as_str())
317        }
318        fn variable_str(&self, name: &str) -> Option<&str> {
319            self.0
320                .get(&format!("variable_{}", name))
321                .map(|s| s.as_str())
322        }
323    }
324
325    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
326        let map: HashMap<String, String> = pairs
327            .iter()
328            .map(|(k, v)| (k.to_string(), v.to_string()))
329            .collect();
330        TestStore(map)
331    }
332
333    #[test]
334    fn header_str_direct() {
335        let s = store_with(&[("Unique-ID", "abc-123")]);
336        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
337        assert_eq!(s.header_str("Missing"), None);
338    }
339
340    #[test]
341    fn header_by_enum() {
342        let s = store_with(&[("Unique-ID", "abc-123")]);
343        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
344    }
345
346    #[test]
347    fn variable_str_direct() {
348        let s = store_with(&[("variable_read_codec", "PCMU")]);
349        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
350        assert_eq!(s.variable_str("missing"), None);
351    }
352
353    #[test]
354    fn variable_by_enum() {
355        let s = store_with(&[("variable_read_codec", "PCMU")]);
356        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
357    }
358
359    #[test]
360    fn unique_id_primary() {
361        let s = store_with(&[("Unique-ID", "uuid-1")]);
362        assert_eq!(s.unique_id(), Some("uuid-1"));
363    }
364
365    #[test]
366    fn unique_id_fallback() {
367        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
368        assert_eq!(s.unique_id(), Some("uuid-2"));
369    }
370
371    #[test]
372    fn unique_id_none() {
373        let s = store_with(&[]);
374        assert_eq!(s.unique_id(), None);
375    }
376
377    #[test]
378    fn job_uuid() {
379        let s = store_with(&[("Job-UUID", "job-1")]);
380        assert_eq!(s.job_uuid(), Some("job-1"));
381    }
382
383    #[test]
384    fn channel_name() {
385        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
386        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
387    }
388
389    #[test]
390    fn caller_id_number_and_name() {
391        let s = store_with(&[
392            ("Caller-Caller-ID-Number", "1000"),
393            ("Caller-Caller-ID-Name", "Alice"),
394        ]);
395        assert_eq!(s.caller_id_number(), Some("1000"));
396        assert_eq!(s.caller_id_name(), Some("Alice"));
397    }
398
399    #[test]
400    fn hangup_cause_typed() {
401        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
402        assert_eq!(
403            s.hangup_cause()
404                .unwrap(),
405            Some(crate::channel::HangupCause::NormalClearing)
406        );
407    }
408
409    #[test]
410    fn hangup_cause_invalid_is_error() {
411        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
412        assert!(s
413            .hangup_cause()
414            .is_err());
415    }
416
417    #[test]
418    fn destination_number() {
419        let s = store_with(&[("Caller-Destination-Number", "1000")]);
420        assert_eq!(s.destination_number(), Some("1000"));
421    }
422
423    #[test]
424    fn callee_id() {
425        let s = store_with(&[
426            ("Caller-Callee-ID-Number", "2000"),
427            ("Caller-Callee-ID-Name", "Bob"),
428        ]);
429        assert_eq!(s.callee_id_number(), Some("2000"));
430        assert_eq!(s.callee_id_name(), Some("Bob"));
431    }
432
433    #[test]
434    fn event_subclass() {
435        let s = store_with(&[("Event-Subclass", "sofia::register")]);
436        assert_eq!(s.event_subclass(), Some("sofia::register"));
437    }
438
439    #[test]
440    fn channel_state_typed() {
441        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
442        assert_eq!(
443            s.channel_state()
444                .unwrap(),
445            Some(ChannelState::CsExecute)
446        );
447    }
448
449    #[test]
450    fn channel_state_number_typed() {
451        let s = store_with(&[("Channel-State-Number", "4")]);
452        assert_eq!(
453            s.channel_state_number()
454                .unwrap(),
455            Some(ChannelState::CsExecute)
456        );
457    }
458
459    #[test]
460    fn call_state_typed() {
461        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
462        assert_eq!(
463            s.call_state()
464                .unwrap(),
465            Some(CallState::Active)
466        );
467    }
468
469    #[test]
470    fn answer_state_typed() {
471        let s = store_with(&[("Answer-State", "answered")]);
472        assert_eq!(
473            s.answer_state()
474                .unwrap(),
475            Some(AnswerState::Answered)
476        );
477    }
478
479    #[test]
480    fn call_direction_typed() {
481        let s = store_with(&[("Call-Direction", "inbound")]);
482        assert_eq!(
483            s.call_direction()
484                .unwrap(),
485            Some(CallDirection::Inbound)
486        );
487    }
488
489    #[test]
490    fn priority_typed() {
491        let s = store_with(&[("priority", "HIGH")]);
492        assert_eq!(
493            s.priority()
494                .unwrap(),
495            Some(EslEventPriority::High)
496        );
497    }
498
499    #[test]
500    fn timetable_extraction() {
501        let s = store_with(&[
502            ("Caller-Channel-Created-Time", "1700000001000000"),
503            ("Caller-Channel-Answered-Time", "1700000005000000"),
504        ]);
505        let tt = s
506            .caller_timetable()
507            .unwrap()
508            .expect("should have timetable");
509        assert_eq!(tt.created, Some(1700000001000000));
510        assert_eq!(tt.answered, Some(1700000005000000));
511        assert_eq!(tt.hungup, None);
512    }
513
514    #[test]
515    fn timetable_other_leg() {
516        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
517        let tt = s
518            .other_leg_timetable()
519            .unwrap()
520            .expect("should have timetable");
521        assert_eq!(tt.created, Some(1700000001000000));
522    }
523
524    #[test]
525    fn timetable_none_when_absent() {
526        let s = store_with(&[]);
527        assert_eq!(
528            s.caller_timetable()
529                .unwrap(),
530            None
531        );
532    }
533
534    #[test]
535    fn timetable_invalid_is_error() {
536        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
537        let err = s
538            .caller_timetable()
539            .unwrap_err();
540        assert_eq!(err.header, "Caller-Channel-Created-Time");
541    }
542
543    #[test]
544    fn missing_headers_return_none() {
545        let s = store_with(&[]);
546        assert_eq!(
547            s.channel_state()
548                .unwrap(),
549            None
550        );
551        assert_eq!(
552            s.channel_state_number()
553                .unwrap(),
554            None
555        );
556        assert_eq!(
557            s.call_state()
558                .unwrap(),
559            None
560        );
561        assert_eq!(
562            s.answer_state()
563                .unwrap(),
564            None
565        );
566        assert_eq!(
567            s.call_direction()
568                .unwrap(),
569            None
570        );
571        assert_eq!(
572            s.priority()
573                .unwrap(),
574            None
575        );
576        assert_eq!(
577            s.hangup_cause()
578                .unwrap(),
579            None
580        );
581        assert_eq!(s.channel_name(), None);
582        assert_eq!(s.caller_id_number(), None);
583        assert_eq!(s.caller_id_name(), None);
584        assert_eq!(s.destination_number(), None);
585        assert_eq!(s.callee_id_number(), None);
586        assert_eq!(s.callee_id_name(), None);
587        assert_eq!(s.event_subclass(), None);
588        assert_eq!(s.job_uuid(), None);
589        assert_eq!(s.pl_data(), None);
590        assert_eq!(s.sip_event(), None);
591        assert_eq!(s.gateway_name(), None);
592        assert_eq!(s.channel_presence_id(), None);
593        assert_eq!(
594            s.presence_call_direction()
595                .unwrap(),
596            None
597        );
598        assert_eq!(s.event_date_timestamp(), None);
599        assert_eq!(s.event_sequence(), None);
600        assert_eq!(s.dtmf_duration(), None);
601        assert_eq!(s.dtmf_source(), None);
602    }
603
604    #[test]
605    fn notify_in_headers() {
606        let s = store_with(&[
607            ("pl_data", r#"{"invite":"INVITE ..."}"#),
608            ("event", "emergency-AbandonedCall"),
609            ("gateway_name", "ng911-bcf"),
610        ]);
611        assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
612        assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
613        assert_eq!(s.gateway_name(), Some("ng911-bcf"));
614    }
615
616    #[test]
617    fn channel_presence_id() {
618        let s = store_with(&[("Channel-Presence-ID", "1000@example.com")]);
619        assert_eq!(s.channel_presence_id(), Some("1000@example.com"));
620    }
621
622    #[test]
623    fn presence_call_direction_typed() {
624        let s = store_with(&[("Presence-Call-Direction", "outbound")]);
625        assert_eq!(
626            s.presence_call_direction()
627                .unwrap(),
628            Some(CallDirection::Outbound)
629        );
630    }
631
632    #[test]
633    fn event_date_timestamp() {
634        let s = store_with(&[("Event-Date-Timestamp", "1700000001000000")]);
635        assert_eq!(s.event_date_timestamp(), Some("1700000001000000"));
636    }
637
638    #[test]
639    fn event_sequence() {
640        let s = store_with(&[("Event-Sequence", "12345")]);
641        assert_eq!(s.event_sequence(), Some("12345"));
642    }
643
644    #[test]
645    fn dtmf_duration() {
646        let s = store_with(&[("DTMF-Duration", "2000")]);
647        assert_eq!(s.dtmf_duration(), Some("2000"));
648    }
649
650    #[test]
651    fn dtmf_source() {
652        let s = store_with(&[("DTMF-Source", "rtp")]);
653        assert_eq!(s.dtmf_source(), Some("rtp"));
654    }
655
656    #[test]
657    fn invalid_values_return_err() {
658        let s = store_with(&[
659            ("Channel-State", "BOGUS"),
660            ("Channel-State-Number", "999"),
661            ("Channel-Call-State", "BOGUS"),
662            ("Answer-State", "bogus"),
663            ("Call-Direction", "bogus"),
664            ("Presence-Call-Direction", "bogus"),
665            ("priority", "BOGUS"),
666            ("Hangup-Cause", "BOGUS"),
667        ]);
668        assert!(s
669            .channel_state()
670            .is_err());
671        assert!(s
672            .channel_state_number()
673            .is_err());
674        assert!(s
675            .call_state()
676            .is_err());
677        assert!(s
678            .answer_state()
679            .is_err());
680        assert!(s
681            .call_direction()
682            .is_err());
683        assert!(s
684            .presence_call_direction()
685            .is_err());
686        assert!(s
687            .priority()
688            .is_err());
689        assert!(s
690            .hangup_cause()
691            .is_err());
692    }
693}