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    /// Parse the `Channel-State` header into a [`ChannelState`].
132    ///
133    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
134    fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
135        match self.header(EventHeader::ChannelState) {
136            Some(s) => Ok(Some(s.parse()?)),
137            None => Ok(None),
138        }
139    }
140
141    /// Parse the `Channel-State-Number` header into a [`ChannelState`].
142    ///
143    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
144    fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
145        match self.header(EventHeader::ChannelStateNumber) {
146            Some(s) => {
147                let n: u8 = s
148                    .parse()
149                    .map_err(|_| ParseChannelStateError(s.to_string()))?;
150                ChannelState::from_number(n)
151                    .ok_or_else(|| ParseChannelStateError(s.to_string()))
152                    .map(Some)
153            }
154            None => Ok(None),
155        }
156    }
157
158    /// Parse the `Channel-Call-State` header into a [`CallState`].
159    ///
160    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
161    fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
162        match self.header(EventHeader::ChannelCallState) {
163            Some(s) => Ok(Some(s.parse()?)),
164            None => Ok(None),
165        }
166    }
167
168    /// Parse the `Answer-State` header into an [`AnswerState`].
169    ///
170    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
171    fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
172        match self.header(EventHeader::AnswerState) {
173            Some(s) => Ok(Some(s.parse()?)),
174            None => Ok(None),
175        }
176    }
177
178    /// Parse the `Call-Direction` header into a [`CallDirection`].
179    ///
180    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
181    fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
182        match self.header(EventHeader::CallDirection) {
183            Some(s) => Ok(Some(s.parse()?)),
184            None => Ok(None),
185        }
186    }
187
188    /// Parse the `priority` header value.
189    ///
190    /// Returns `Ok(None)` if the header is absent, `Err` if present but unparseable.
191    fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
192        match self.header(EventHeader::Priority) {
193            Some(s) => Ok(Some(s.parse()?)),
194            None => Ok(None),
195        }
196    }
197
198    /// Extract timetable from timestamp headers with the given prefix.
199    ///
200    /// Returns `Ok(None)` if no timestamp headers with this prefix are present.
201    /// Returns `Err` if a header is present but contains an invalid value.
202    fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
203        ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
204    }
205
206    /// Caller-leg channel timetable (`Caller-*-Time` headers).
207    fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
208        self.timetable("Caller")
209    }
210
211    /// Other-leg channel timetable (`Other-Leg-*-Time` headers).
212    fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
213        self.timetable("Other-Leg")
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::variables::ChannelVariable;
221    use std::collections::HashMap;
222
223    struct TestStore(HashMap<String, String>);
224
225    impl HeaderLookup for TestStore {
226        fn header_str(&self, name: &str) -> Option<&str> {
227            self.0
228                .get(name)
229                .map(|s| s.as_str())
230        }
231        fn variable_str(&self, name: &str) -> Option<&str> {
232            self.0
233                .get(&format!("variable_{}", name))
234                .map(|s| s.as_str())
235        }
236    }
237
238    fn store_with(pairs: &[(&str, &str)]) -> TestStore {
239        let map: HashMap<String, String> = pairs
240            .iter()
241            .map(|(k, v)| (k.to_string(), v.to_string()))
242            .collect();
243        TestStore(map)
244    }
245
246    #[test]
247    fn header_str_direct() {
248        let s = store_with(&[("Unique-ID", "abc-123")]);
249        assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
250        assert_eq!(s.header_str("Missing"), None);
251    }
252
253    #[test]
254    fn header_by_enum() {
255        let s = store_with(&[("Unique-ID", "abc-123")]);
256        assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
257    }
258
259    #[test]
260    fn variable_str_direct() {
261        let s = store_with(&[("variable_read_codec", "PCMU")]);
262        assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
263        assert_eq!(s.variable_str("missing"), None);
264    }
265
266    #[test]
267    fn variable_by_enum() {
268        let s = store_with(&[("variable_read_codec", "PCMU")]);
269        assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
270    }
271
272    #[test]
273    fn unique_id_primary() {
274        let s = store_with(&[("Unique-ID", "uuid-1")]);
275        assert_eq!(s.unique_id(), Some("uuid-1"));
276    }
277
278    #[test]
279    fn unique_id_fallback() {
280        let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
281        assert_eq!(s.unique_id(), Some("uuid-2"));
282    }
283
284    #[test]
285    fn unique_id_none() {
286        let s = store_with(&[]);
287        assert_eq!(s.unique_id(), None);
288    }
289
290    #[test]
291    fn job_uuid() {
292        let s = store_with(&[("Job-UUID", "job-1")]);
293        assert_eq!(s.job_uuid(), Some("job-1"));
294    }
295
296    #[test]
297    fn channel_name() {
298        let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
299        assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
300    }
301
302    #[test]
303    fn caller_id_number_and_name() {
304        let s = store_with(&[
305            ("Caller-Caller-ID-Number", "1000"),
306            ("Caller-Caller-ID-Name", "Alice"),
307        ]);
308        assert_eq!(s.caller_id_number(), Some("1000"));
309        assert_eq!(s.caller_id_name(), Some("Alice"));
310    }
311
312    #[test]
313    fn hangup_cause_typed() {
314        let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
315        assert_eq!(
316            s.hangup_cause()
317                .unwrap(),
318            Some(crate::channel::HangupCause::NormalClearing)
319        );
320    }
321
322    #[test]
323    fn hangup_cause_invalid_is_error() {
324        let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
325        assert!(s
326            .hangup_cause()
327            .is_err());
328    }
329
330    #[test]
331    fn destination_number() {
332        let s = store_with(&[("Caller-Destination-Number", "1000")]);
333        assert_eq!(s.destination_number(), Some("1000"));
334    }
335
336    #[test]
337    fn callee_id() {
338        let s = store_with(&[
339            ("Caller-Callee-ID-Number", "2000"),
340            ("Caller-Callee-ID-Name", "Bob"),
341        ]);
342        assert_eq!(s.callee_id_number(), Some("2000"));
343        assert_eq!(s.callee_id_name(), Some("Bob"));
344    }
345
346    #[test]
347    fn event_subclass() {
348        let s = store_with(&[("Event-Subclass", "sofia::register")]);
349        assert_eq!(s.event_subclass(), Some("sofia::register"));
350    }
351
352    #[test]
353    fn channel_state_typed() {
354        let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
355        assert_eq!(
356            s.channel_state()
357                .unwrap(),
358            Some(ChannelState::CsExecute)
359        );
360    }
361
362    #[test]
363    fn channel_state_number_typed() {
364        let s = store_with(&[("Channel-State-Number", "4")]);
365        assert_eq!(
366            s.channel_state_number()
367                .unwrap(),
368            Some(ChannelState::CsExecute)
369        );
370    }
371
372    #[test]
373    fn call_state_typed() {
374        let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
375        assert_eq!(
376            s.call_state()
377                .unwrap(),
378            Some(CallState::Active)
379        );
380    }
381
382    #[test]
383    fn answer_state_typed() {
384        let s = store_with(&[("Answer-State", "answered")]);
385        assert_eq!(
386            s.answer_state()
387                .unwrap(),
388            Some(AnswerState::Answered)
389        );
390    }
391
392    #[test]
393    fn call_direction_typed() {
394        let s = store_with(&[("Call-Direction", "inbound")]);
395        assert_eq!(
396            s.call_direction()
397                .unwrap(),
398            Some(CallDirection::Inbound)
399        );
400    }
401
402    #[test]
403    fn priority_typed() {
404        let s = store_with(&[("priority", "HIGH")]);
405        assert_eq!(
406            s.priority()
407                .unwrap(),
408            Some(EslEventPriority::High)
409        );
410    }
411
412    #[test]
413    fn timetable_extraction() {
414        let s = store_with(&[
415            ("Caller-Channel-Created-Time", "1700000001000000"),
416            ("Caller-Channel-Answered-Time", "1700000005000000"),
417        ]);
418        let tt = s
419            .caller_timetable()
420            .unwrap()
421            .expect("should have timetable");
422        assert_eq!(tt.created, Some(1700000001000000));
423        assert_eq!(tt.answered, Some(1700000005000000));
424        assert_eq!(tt.hungup, None);
425    }
426
427    #[test]
428    fn timetable_other_leg() {
429        let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
430        let tt = s
431            .other_leg_timetable()
432            .unwrap()
433            .expect("should have timetable");
434        assert_eq!(tt.created, Some(1700000001000000));
435    }
436
437    #[test]
438    fn timetable_none_when_absent() {
439        let s = store_with(&[]);
440        assert_eq!(
441            s.caller_timetable()
442                .unwrap(),
443            None
444        );
445    }
446
447    #[test]
448    fn timetable_invalid_is_error() {
449        let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
450        let err = s
451            .caller_timetable()
452            .unwrap_err();
453        assert_eq!(err.header, "Caller-Channel-Created-Time");
454    }
455
456    #[test]
457    fn missing_headers_return_none() {
458        let s = store_with(&[]);
459        assert_eq!(
460            s.channel_state()
461                .unwrap(),
462            None
463        );
464        assert_eq!(
465            s.channel_state_number()
466                .unwrap(),
467            None
468        );
469        assert_eq!(
470            s.call_state()
471                .unwrap(),
472            None
473        );
474        assert_eq!(
475            s.answer_state()
476                .unwrap(),
477            None
478        );
479        assert_eq!(
480            s.call_direction()
481                .unwrap(),
482            None
483        );
484        assert_eq!(
485            s.priority()
486                .unwrap(),
487            None
488        );
489        assert_eq!(
490            s.hangup_cause()
491                .unwrap(),
492            None
493        );
494        assert_eq!(s.channel_name(), None);
495        assert_eq!(s.caller_id_number(), None);
496        assert_eq!(s.caller_id_name(), None);
497        assert_eq!(s.destination_number(), None);
498        assert_eq!(s.callee_id_number(), None);
499        assert_eq!(s.callee_id_name(), None);
500        assert_eq!(s.event_subclass(), None);
501        assert_eq!(s.job_uuid(), None);
502    }
503
504    #[test]
505    fn invalid_values_return_err() {
506        let s = store_with(&[
507            ("Channel-State", "BOGUS"),
508            ("Channel-State-Number", "999"),
509            ("Channel-Call-State", "BOGUS"),
510            ("Answer-State", "bogus"),
511            ("Call-Direction", "bogus"),
512            ("priority", "BOGUS"),
513            ("Hangup-Cause", "BOGUS"),
514        ]);
515        assert!(s
516            .channel_state()
517            .is_err());
518        assert!(s
519            .channel_state_number()
520            .is_err());
521        assert!(s
522            .call_state()
523            .is_err());
524        assert!(s
525            .answer_state()
526            .is_err());
527        assert!(s
528            .call_direction()
529            .is_err());
530        assert!(s
531            .priority()
532            .is_err());
533        assert!(s
534            .hangup_cause()
535            .is_err());
536    }
537}