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