Skip to main content

freeswitch_types/commands/endpoint/
mod.rs

1//! FreeSWITCH endpoint types for originate and bridge dial strings.
2//!
3//! Each endpoint type corresponds to a real FreeSWITCH endpoint module
4//! or runtime expression. Concrete structs implement the [`DialString`]
5//! trait independently; the [`Endpoint`] enum wraps them for
6//! serialization and polymorphic storage.
7
8mod audio;
9mod error;
10mod group_call;
11mod loopback;
12mod sofia;
13mod user;
14
15pub use audio::AudioEndpoint;
16pub use error::ErrorEndpoint;
17pub use group_call::{GroupCall, GroupCallOrder, ParseGroupCallOrderError};
18pub use loopback::LoopbackEndpoint;
19pub use sofia::{SofiaContact, SofiaEndpoint, SofiaGateway};
20pub use user::UserEndpoint;
21
22use std::fmt;
23use std::str::FromStr;
24
25use super::find_matching_bracket;
26use super::originate::{OriginateError, Variables};
27
28/// Common interface for anything that formats as a FreeSWITCH dial string.
29///
30/// Implemented on each concrete endpoint struct and on the [`Endpoint`] enum.
31/// Downstream crates can implement this on custom endpoint types.
32pub trait DialString: fmt::Display {
33    /// Per-endpoint variables, if any.
34    fn variables(&self) -> Option<&Variables>;
35    /// Mutable access to per-endpoint variables.
36    fn variables_mut(&mut self) -> Option<&mut Variables>;
37    /// Replace per-endpoint variables.
38    fn set_variables(&mut self, vars: Option<Variables>);
39}
40
41// ---------------------------------------------------------------------------
42// Helpers
43// ---------------------------------------------------------------------------
44
45fn write_variables(f: &mut fmt::Formatter<'_>, vars: &Option<Variables>) -> fmt::Result {
46    if let Some(vars) = vars {
47        if !vars.is_empty() {
48            write!(f, "{}", vars)?;
49        }
50    }
51    Ok(())
52}
53
54/// Strip the leading variable block then a fixed endpoint prefix.
55///
56/// Returns `(variables, rest_after_prefix)`. The `kind` label is used in the
57/// `not a {kind} endpoint` error when the prefix is missing.
58pub(super) fn strip_endpoint_prefix<'a>(
59    s: &'a str,
60    prefix: &str,
61    kind: &str,
62) -> Result<(Option<Variables>, &'a str), OriginateError> {
63    let (variables, uri) = extract_variables(s)?;
64    let rest = uri
65        .strip_prefix(prefix)
66        .ok_or_else(|| OriginateError::ParseError(format!("not a {} endpoint", kind)))?;
67    Ok((variables, rest))
68}
69
70/// Extract a leading variable block (`{...}`, `[...]`, or `<...>`) from a
71/// dial string, returning the parsed variables and the remaining URI portion.
72///
73/// Uses depth-aware bracket matching so nested brackets in values (e.g.
74/// `<sip_h_Call-Info=<url>>`) don't cause premature closure.
75fn extract_variables(s: &str) -> Result<(Option<Variables>, &str), OriginateError> {
76    let (open, close_ch) = match s
77        .as_bytes()
78        .first()
79    {
80        Some(b'{') => ('{', '}'),
81        Some(b'[') => ('[', ']'),
82        Some(b'<') => ('<', '>'),
83        _ => return Ok((None, s)),
84    };
85    let close = find_matching_bracket(s, open, close_ch)
86        .ok_or_else(|| OriginateError::ParseError(format!("unclosed {} in endpoint", open)))?;
87    let var_str = &s[..=close];
88    let vars: Variables = var_str.parse()?;
89    let vars = if vars.is_empty() { None } else { Some(vars) };
90    Ok((vars, s[close + 1..].trim()))
91}
92
93// ---------------------------------------------------------------------------
94// Endpoint enum
95// ---------------------------------------------------------------------------
96
97/// Polymorphic endpoint wrapping all concrete types.
98///
99/// Use this in [`Originate`](super::originate::Originate) and
100/// [`BridgeDialString`](super::bridge::BridgeDialString) where any endpoint type must be accepted.
101#[derive(Debug, Clone, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
104#[non_exhaustive]
105pub enum Endpoint {
106    /// `sofia/{profile}/{destination}`
107    Sofia(SofiaEndpoint),
108    /// `sofia/gateway/[{profile}::]{gateway}/{destination}`
109    SofiaGateway(SofiaGateway),
110    /// `loopback/{extension}[/{context}]`
111    Loopback(LoopbackEndpoint),
112    /// `user/{name}[@{domain}]`
113    User(UserEndpoint),
114    /// `${sofia_contact([profile/]user@domain)}`
115    SofiaContact(SofiaContact),
116    /// `${group_call(group@domain[+order])}`
117    GroupCall(GroupCall),
118    /// `error/{cause}`
119    Error(ErrorEndpoint),
120    /// `portaudio[/{destination}]`
121    #[cfg_attr(feature = "serde", serde(rename = "portaudio"))]
122    PortAudio(AudioEndpoint),
123    /// `pulseaudio[/{destination}]`
124    #[cfg_attr(feature = "serde", serde(rename = "pulseaudio"))]
125    PulseAudio(AudioEndpoint),
126    /// `alsa[/{destination}]`
127    Alsa(AudioEndpoint),
128}
129
130// ---------------------------------------------------------------------------
131// From impls
132// ---------------------------------------------------------------------------
133
134impl From<SofiaEndpoint> for Endpoint {
135    fn from(ep: SofiaEndpoint) -> Self {
136        Self::Sofia(ep)
137    }
138}
139
140impl From<SofiaGateway> for Endpoint {
141    fn from(ep: SofiaGateway) -> Self {
142        Self::SofiaGateway(ep)
143    }
144}
145
146impl From<LoopbackEndpoint> for Endpoint {
147    fn from(ep: LoopbackEndpoint) -> Self {
148        Self::Loopback(ep)
149    }
150}
151
152impl From<UserEndpoint> for Endpoint {
153    fn from(ep: UserEndpoint) -> Self {
154        Self::User(ep)
155    }
156}
157
158impl From<SofiaContact> for Endpoint {
159    fn from(ep: SofiaContact) -> Self {
160        Self::SofiaContact(ep)
161    }
162}
163
164impl From<GroupCall> for Endpoint {
165    fn from(ep: GroupCall) -> Self {
166        Self::GroupCall(ep)
167    }
168}
169
170impl From<ErrorEndpoint> for Endpoint {
171    fn from(ep: ErrorEndpoint) -> Self {
172        Self::Error(ep)
173    }
174}
175
176// ---------------------------------------------------------------------------
177// Display
178// ---------------------------------------------------------------------------
179
180impl fmt::Display for Endpoint {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        match self {
183            Self::Sofia(ep) => ep.fmt(f),
184            Self::SofiaGateway(ep) => ep.fmt(f),
185            Self::Loopback(ep) => ep.fmt(f),
186            Self::User(ep) => ep.fmt(f),
187            Self::SofiaContact(ep) => ep.fmt(f),
188            Self::GroupCall(ep) => ep.fmt(f),
189            Self::Error(ep) => ep.fmt(f),
190            Self::PortAudio(ep) => ep.fmt_with_prefix(f, "portaudio"),
191            Self::PulseAudio(ep) => ep.fmt_with_prefix(f, "pulseaudio"),
192            Self::Alsa(ep) => ep.fmt_with_prefix(f, "alsa"),
193        }
194    }
195}
196
197// ---------------------------------------------------------------------------
198// FromStr
199// ---------------------------------------------------------------------------
200
201impl FromStr for Endpoint {
202    type Err = OriginateError;
203
204    fn from_str(s: &str) -> Result<Self, Self::Err> {
205        let (variables, uri) = extract_variables(s)?;
206        // Re-assemble with variables for individual FromStr impls
207        let full = if variables.is_some() {
208            s.to_string()
209        } else {
210            uri.to_string()
211        };
212
213        if uri.starts_with("${sofia_contact(") {
214            Ok(Self::SofiaContact(full.parse()?))
215        } else if uri.starts_with("${group_call(") {
216            Ok(Self::GroupCall(full.parse()?))
217        } else if uri.starts_with("error/") {
218            Ok(Self::Error(full.parse()?))
219        } else if uri.starts_with("loopback/") {
220            Ok(Self::Loopback(full.parse()?))
221        } else if uri.starts_with("sofia/gateway/") {
222            Ok(Self::SofiaGateway(full.parse()?))
223        } else if uri.starts_with("sofia/") {
224            Ok(Self::Sofia(full.parse()?))
225        } else if uri.starts_with("user/") {
226            Ok(Self::User(full.parse()?))
227        } else if uri.starts_with("portaudio") {
228            Ok(Self::PortAudio(AudioEndpoint::parse_with_prefix(
229                &full,
230                "portaudio",
231            )?))
232        } else if uri.starts_with("pulseaudio") {
233            Ok(Self::PulseAudio(AudioEndpoint::parse_with_prefix(
234                &full,
235                "pulseaudio",
236            )?))
237        } else if uri.starts_with("alsa") {
238            Ok(Self::Alsa(AudioEndpoint::parse_with_prefix(&full, "alsa")?))
239        } else {
240            Err(OriginateError::ParseError(format!(
241                "unknown endpoint type: {}",
242                uri
243            )))
244        }
245    }
246}
247
248// ---------------------------------------------------------------------------
249// DialString impls
250// ---------------------------------------------------------------------------
251
252macro_rules! impl_dial_string_with_variables {
253    ($ty:ty) => {
254        impl DialString for $ty {
255            fn variables(&self) -> Option<&Variables> {
256                self.variables
257                    .as_ref()
258            }
259            fn variables_mut(&mut self) -> Option<&mut Variables> {
260                self.variables
261                    .as_mut()
262            }
263            fn set_variables(&mut self, vars: Option<Variables>) {
264                self.variables = vars;
265            }
266        }
267    };
268}
269
270impl_dial_string_with_variables!(SofiaEndpoint);
271impl_dial_string_with_variables!(SofiaGateway);
272impl_dial_string_with_variables!(LoopbackEndpoint);
273impl_dial_string_with_variables!(UserEndpoint);
274impl_dial_string_with_variables!(SofiaContact);
275impl_dial_string_with_variables!(GroupCall);
276impl_dial_string_with_variables!(AudioEndpoint);
277
278impl DialString for ErrorEndpoint {
279    fn variables(&self) -> Option<&Variables> {
280        None
281    }
282    fn variables_mut(&mut self) -> Option<&mut Variables> {
283        None
284    }
285    fn set_variables(&mut self, _vars: Option<Variables>) {}
286}
287
288impl DialString for Endpoint {
289    fn variables(&self) -> Option<&Variables> {
290        match self {
291            Self::Sofia(ep) => ep.variables(),
292            Self::SofiaGateway(ep) => ep.variables(),
293            Self::Loopback(ep) => ep.variables(),
294            Self::User(ep) => ep.variables(),
295            Self::SofiaContact(ep) => ep.variables(),
296            Self::GroupCall(ep) => ep.variables(),
297            Self::Error(ep) => ep.variables(),
298            Self::PortAudio(ep) | Self::PulseAudio(ep) | Self::Alsa(ep) => ep.variables(),
299        }
300    }
301    fn variables_mut(&mut self) -> Option<&mut Variables> {
302        match self {
303            Self::Sofia(ep) => ep.variables_mut(),
304            Self::SofiaGateway(ep) => ep.variables_mut(),
305            Self::Loopback(ep) => ep.variables_mut(),
306            Self::User(ep) => ep.variables_mut(),
307            Self::SofiaContact(ep) => ep.variables_mut(),
308            Self::GroupCall(ep) => ep.variables_mut(),
309            Self::Error(ep) => ep.variables_mut(),
310            Self::PortAudio(ep) | Self::PulseAudio(ep) | Self::Alsa(ep) => ep.variables_mut(),
311        }
312    }
313    fn set_variables(&mut self, vars: Option<Variables>) {
314        match self {
315            Self::Sofia(ep) => ep.set_variables(vars),
316            Self::SofiaGateway(ep) => ep.set_variables(vars),
317            Self::Loopback(ep) => ep.set_variables(vars),
318            Self::User(ep) => ep.set_variables(vars),
319            Self::SofiaContact(ep) => ep.set_variables(vars),
320            Self::GroupCall(ep) => ep.set_variables(vars),
321            Self::Error(ep) => ep.set_variables(vars),
322            Self::PortAudio(ep) | Self::PulseAudio(ep) | Self::Alsa(ep) => ep.set_variables(vars),
323        }
324    }
325}
326
327// ---------------------------------------------------------------------------
328// Tests
329// ---------------------------------------------------------------------------
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::commands::originate::VariablesType;
335
336    // --- extract_variables depth-aware bracket matching ---
337
338    #[test]
339    fn extract_variables_nested_angle_brackets() {
340        let (vars, rest) = extract_variables("<sip_h_Call-Info=<url>>sofia/gw/x").unwrap();
341        assert_eq!(rest, "sofia/gw/x");
342        assert!(vars.is_some());
343    }
344
345    #[test]
346    fn extract_variables_nested_curly_brackets() {
347        let (vars, rest) = extract_variables("{a={b}}sofia/internal/1000").unwrap();
348        assert_eq!(rest, "sofia/internal/1000");
349        assert!(vars.is_some());
350    }
351
352    #[test]
353    fn extract_variables_unclosed_returns_error() {
354        let result = extract_variables("{a=b");
355        assert!(result.is_err());
356    }
357
358    // --- Endpoint enum FromStr dispatch ---
359
360    #[test]
361    fn endpoint_from_str_sofia() {
362        let ep: Endpoint = "sofia/internal/1000@example.com"
363            .parse()
364            .unwrap();
365        assert!(matches!(ep, Endpoint::Sofia(_)));
366    }
367
368    #[test]
369    fn endpoint_from_str_sofia_gateway() {
370        let ep: Endpoint = "sofia/gateway/my_gw/1234"
371            .parse()
372            .unwrap();
373        assert!(matches!(ep, Endpoint::SofiaGateway(_)));
374    }
375
376    #[test]
377    fn endpoint_from_str_loopback() {
378        let ep: Endpoint = "loopback/9199/test"
379            .parse()
380            .unwrap();
381        assert!(matches!(ep, Endpoint::Loopback(_)));
382    }
383
384    #[test]
385    fn endpoint_from_str_user() {
386        let ep: Endpoint = "user/1000@example.com"
387            .parse()
388            .unwrap();
389        assert!(matches!(ep, Endpoint::User(_)));
390    }
391
392    #[test]
393    fn endpoint_from_str_sofia_contact() {
394        let ep: Endpoint = "${sofia_contact(1000@example.com)}"
395            .parse()
396            .unwrap();
397        assert!(matches!(ep, Endpoint::SofiaContact(_)));
398    }
399
400    #[test]
401    fn endpoint_from_str_group_call() {
402        let ep: Endpoint = "${group_call(support@example.com+A)}"
403            .parse()
404            .unwrap();
405        assert!(matches!(ep, Endpoint::GroupCall(_)));
406    }
407
408    #[test]
409    fn endpoint_from_str_error() {
410        let ep: Endpoint = "error/USER_BUSY"
411            .parse()
412            .unwrap();
413        assert!(matches!(ep, Endpoint::Error(_)));
414        assert!("error/user_busy"
415            .parse::<Endpoint>()
416            .is_err());
417    }
418
419    #[test]
420    fn endpoint_from_str_unknown_errors() {
421        let result = "verto/1234".parse::<Endpoint>();
422        assert!(result.is_err());
423    }
424
425    #[test]
426    fn endpoint_from_str_with_variables() {
427        let ep: Endpoint = "{timeout=30}sofia/internal/1000@example.com"
428            .parse()
429            .unwrap();
430        if let Endpoint::Sofia(inner) = &ep {
431            assert_eq!(inner.profile, "internal");
432            assert!(inner
433                .variables
434                .is_some());
435        } else {
436            panic!("expected Sofia variant");
437        }
438    }
439
440    // --- Display delegation ---
441
442    #[test]
443    fn endpoint_display_delegates_to_inner() {
444        let ep = Endpoint::Sofia(SofiaEndpoint {
445            profile: "internal".into(),
446            destination: "1000@example.com".into(),
447            variables: None,
448        });
449        assert_eq!(ep.to_string(), "sofia/internal/1000@example.com");
450    }
451
452    // --- DialString trait ---
453
454    #[test]
455    fn dial_string_variables_returns_some() {
456        let mut vars = Variables::new(VariablesType::Default);
457        vars.insert("k", "v");
458        let ep = SofiaEndpoint {
459            profile: "internal".into(),
460            destination: "1000".into(),
461            variables: Some(vars),
462        };
463        assert!(ep
464            .variables()
465            .is_some());
466        assert_eq!(
467            ep.variables()
468                .unwrap()
469                .get("k"),
470            Some("v")
471        );
472    }
473
474    #[test]
475    fn dial_string_variables_returns_none() {
476        let ep = SofiaEndpoint {
477            profile: "internal".into(),
478            destination: "1000".into(),
479            variables: None,
480        };
481        assert!(ep
482            .variables()
483            .is_none());
484    }
485
486    #[test]
487    fn dial_string_set_variables() {
488        let mut ep = SofiaEndpoint {
489            profile: "internal".into(),
490            destination: "1000".into(),
491            variables: None,
492        };
493        let mut vars = Variables::new(VariablesType::Channel);
494        vars.insert("k", "v");
495        ep.set_variables(Some(vars));
496        assert!(ep
497            .variables()
498            .is_some());
499    }
500
501    #[test]
502    fn dial_string_error_endpoint_no_variables() {
503        let ep = ErrorEndpoint::new(crate::channel::HangupCause::UserBusy);
504        assert!(ep
505            .variables()
506            .is_none());
507    }
508
509    #[test]
510    fn dial_string_on_endpoint_enum() {
511        let mut vars = Variables::new(VariablesType::Default);
512        vars.insert("k", "v");
513        let ep = Endpoint::Sofia(SofiaEndpoint {
514            profile: "internal".into(),
515            destination: "1000".into(),
516            variables: Some(vars),
517        });
518        assert!(ep
519            .variables()
520            .is_some());
521    }
522
523    // --- Serde: Endpoint enum ---
524
525    #[test]
526    fn serde_endpoint_enum_sofia() {
527        let ep = Endpoint::Sofia(SofiaEndpoint {
528            profile: "internal".into(),
529            destination: "1000@example.com".into(),
530            variables: None,
531        });
532        let json = serde_json::to_string(&ep).unwrap();
533        assert!(json.contains("\"sofia\""));
534        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
535        assert_eq!(parsed, ep);
536    }
537
538    #[test]
539    fn serde_endpoint_enum_sofia_gateway() {
540        let ep = Endpoint::SofiaGateway(SofiaGateway {
541            gateway: "gw1".into(),
542            destination: "1234".into(),
543            profile: None,
544            variables: None,
545        });
546        let json = serde_json::to_string(&ep).unwrap();
547        assert!(json.contains("\"sofia_gateway\""));
548        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
549        assert_eq!(parsed, ep);
550    }
551
552    #[test]
553    fn serde_endpoint_enum_loopback() {
554        let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
555        let json = serde_json::to_string(&ep).unwrap();
556        assert!(json.contains("\"loopback\""));
557        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
558        assert_eq!(parsed, ep);
559    }
560
561    #[test]
562    fn serde_endpoint_enum_user() {
563        let ep = Endpoint::User(UserEndpoint {
564            name: "bob".into(),
565            domain: Some("example.com".into()),
566            variables: None,
567        });
568        let json = serde_json::to_string(&ep).unwrap();
569        assert!(json.contains("\"user\""));
570        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
571        assert_eq!(parsed, ep);
572    }
573
574    #[test]
575    fn serde_endpoint_enum_sofia_contact() {
576        let ep = Endpoint::SofiaContact(SofiaContact {
577            user: "1000".into(),
578            domain: "example.com".into(),
579            profile: None,
580            variables: None,
581        });
582        let json = serde_json::to_string(&ep).unwrap();
583        assert!(json.contains("\"sofia_contact\""));
584        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
585        assert_eq!(parsed, ep);
586    }
587
588    #[test]
589    fn serde_endpoint_enum_group_call() {
590        let ep = Endpoint::GroupCall(GroupCall {
591            group: "support".into(),
592            domain: "example.com".into(),
593            order: Some(GroupCallOrder::All),
594            variables: None,
595        });
596        let json = serde_json::to_string(&ep).unwrap();
597        assert!(json.contains("\"group_call\""));
598        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
599        assert_eq!(parsed, ep);
600    }
601
602    #[test]
603    fn serde_endpoint_enum_error() {
604        let ep = Endpoint::Error(ErrorEndpoint::new(crate::channel::HangupCause::UserBusy));
605        let json = serde_json::to_string(&ep).unwrap();
606        assert!(json.contains("\"error\""));
607        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
608        assert_eq!(parsed, ep);
609    }
610
611    #[test]
612    fn serde_endpoint_skips_none_variables() {
613        let ep = SofiaEndpoint {
614            profile: "internal".into(),
615            destination: "1000".into(),
616            variables: None,
617        };
618        let json = serde_json::to_string(&ep).unwrap();
619        assert!(!json.contains("variables"));
620    }
621
622    #[test]
623    fn serde_endpoint_skips_none_profile() {
624        let ep = SofiaGateway {
625            gateway: "gw".into(),
626            destination: "1234".into(),
627            profile: None,
628            variables: None,
629        };
630        let json = serde_json::to_string(&ep).unwrap();
631        assert!(!json.contains("profile"));
632    }
633
634    // --- Audio endpoints through Endpoint enum ---
635
636    #[test]
637    fn portaudio_display() {
638        let ep = AudioEndpoint {
639            destination: Some("auto_answer".into()),
640            variables: None,
641        };
642        let endpoint = Endpoint::PortAudio(ep);
643        assert_eq!(endpoint.to_string(), "portaudio/auto_answer");
644    }
645
646    #[test]
647    fn portaudio_bare_display() {
648        let ep = AudioEndpoint {
649            destination: None,
650            variables: None,
651        };
652        let endpoint = Endpoint::PortAudio(ep);
653        assert_eq!(endpoint.to_string(), "portaudio");
654    }
655
656    #[test]
657    fn portaudio_from_str() {
658        let ep: Endpoint = "portaudio/auto_answer"
659            .parse()
660            .unwrap();
661        if let Endpoint::PortAudio(audio) = ep {
662            assert_eq!(
663                audio
664                    .destination
665                    .as_deref(),
666                Some("auto_answer")
667            );
668        } else {
669            panic!("expected PortAudio");
670        }
671    }
672
673    #[test]
674    fn portaudio_bare_from_str() {
675        let ep: Endpoint = "portaudio"
676            .parse()
677            .unwrap();
678        if let Endpoint::PortAudio(audio) = ep {
679            assert!(audio
680                .destination
681                .is_none());
682        } else {
683            panic!("expected PortAudio");
684        }
685    }
686
687    #[test]
688    fn portaudio_round_trip() {
689        let input = "portaudio/auto_answer";
690        let ep: Endpoint = input
691            .parse()
692            .unwrap();
693        assert_eq!(ep.to_string(), input);
694    }
695
696    #[test]
697    fn portaudio_bare_round_trip() {
698        let input = "portaudio";
699        let ep: Endpoint = input
700            .parse()
701            .unwrap();
702        assert_eq!(ep.to_string(), input);
703    }
704
705    #[test]
706    fn portaudio_with_variables() {
707        let mut vars = Variables::new(VariablesType::Default);
708        vars.insert("codec", "PCMU");
709        let ep = Endpoint::PortAudio(AudioEndpoint {
710            destination: Some("auto_answer".into()),
711            variables: Some(vars),
712        });
713        assert_eq!(ep.to_string(), "{codec=PCMU}portaudio/auto_answer");
714        let parsed: Endpoint = ep
715            .to_string()
716            .parse()
717            .unwrap();
718        assert_eq!(parsed, ep);
719    }
720
721    #[test]
722    fn pulseaudio_display() {
723        let ep = Endpoint::PulseAudio(AudioEndpoint {
724            destination: Some("auto_answer".into()),
725            variables: None,
726        });
727        assert_eq!(ep.to_string(), "pulseaudio/auto_answer");
728    }
729
730    #[test]
731    fn pulseaudio_from_str() {
732        let ep: Endpoint = "pulseaudio/auto_answer"
733            .parse()
734            .unwrap();
735        assert!(matches!(ep, Endpoint::PulseAudio(_)));
736    }
737
738    #[test]
739    fn pulseaudio_round_trip() {
740        let input = "pulseaudio/auto_answer";
741        let ep: Endpoint = input
742            .parse()
743            .unwrap();
744        assert_eq!(ep.to_string(), input);
745    }
746
747    #[test]
748    fn alsa_display() {
749        let ep = Endpoint::Alsa(AudioEndpoint {
750            destination: Some("auto_answer".into()),
751            variables: None,
752        });
753        assert_eq!(ep.to_string(), "alsa/auto_answer");
754    }
755
756    #[test]
757    fn alsa_from_str() {
758        let ep: Endpoint = "alsa/auto_answer"
759            .parse()
760            .unwrap();
761        assert!(matches!(ep, Endpoint::Alsa(_)));
762    }
763
764    #[test]
765    fn alsa_bare_round_trip() {
766        let input = "alsa";
767        let ep: Endpoint = input
768            .parse()
769            .unwrap();
770        assert_eq!(ep.to_string(), input);
771    }
772
773    #[test]
774    fn serde_portaudio() {
775        let ep = Endpoint::PortAudio(AudioEndpoint {
776            destination: Some("auto_answer".into()),
777            variables: None,
778        });
779        let json = serde_json::to_string(&ep).unwrap();
780        assert!(json.contains("\"portaudio\""));
781        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
782        assert_eq!(parsed, ep);
783    }
784
785    #[test]
786    fn serde_pulseaudio() {
787        let ep = Endpoint::PulseAudio(AudioEndpoint {
788            destination: Some("auto_answer".into()),
789            variables: None,
790        });
791        let json = serde_json::to_string(&ep).unwrap();
792        assert!(json.contains("\"pulseaudio\""));
793        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
794        assert_eq!(parsed, ep);
795    }
796
797    #[test]
798    fn serde_alsa() {
799        let ep = Endpoint::Alsa(AudioEndpoint {
800            destination: None,
801            variables: None,
802        });
803        let json = serde_json::to_string(&ep).unwrap();
804        assert!(json.contains("\"alsa\""));
805        let parsed: Endpoint = serde_json::from_str(&json).unwrap();
806        assert_eq!(parsed, ep);
807    }
808
809    // --- From impls ---
810
811    #[test]
812    fn from_sofia_endpoint() {
813        let inner = SofiaEndpoint {
814            profile: "internal".into(),
815            destination: "1000@example.com".into(),
816            variables: None,
817        };
818        let ep: Endpoint = inner
819            .clone()
820            .into();
821        assert_eq!(ep, Endpoint::Sofia(inner));
822    }
823
824    #[test]
825    fn from_sofia_gateway() {
826        let inner = SofiaGateway {
827            gateway: "gw1".into(),
828            destination: "1234".into(),
829            profile: None,
830            variables: None,
831        };
832        let ep: Endpoint = inner
833            .clone()
834            .into();
835        assert_eq!(ep, Endpoint::SofiaGateway(inner));
836    }
837
838    #[test]
839    fn from_loopback_endpoint() {
840        let inner = LoopbackEndpoint::new("9199").with_context("default");
841        let ep: Endpoint = inner
842            .clone()
843            .into();
844        assert_eq!(ep, Endpoint::Loopback(inner));
845    }
846
847    #[test]
848    fn from_user_endpoint() {
849        let inner = UserEndpoint {
850            name: "bob".into(),
851            domain: Some("example.com".into()),
852            variables: None,
853        };
854        let ep: Endpoint = inner
855            .clone()
856            .into();
857        assert_eq!(ep, Endpoint::User(inner));
858    }
859
860    #[test]
861    fn from_sofia_contact() {
862        let inner = SofiaContact {
863            user: "1000".into(),
864            domain: "example.com".into(),
865            profile: None,
866            variables: None,
867        };
868        let ep: Endpoint = inner
869            .clone()
870            .into();
871        assert_eq!(ep, Endpoint::SofiaContact(inner));
872    }
873
874    #[test]
875    fn from_group_call() {
876        let inner = GroupCall::new("support", "example.com").with_order(GroupCallOrder::All);
877        let ep: Endpoint = inner
878            .clone()
879            .into();
880        assert_eq!(ep, Endpoint::GroupCall(inner));
881    }
882
883    #[test]
884    fn from_error_endpoint() {
885        let inner = ErrorEndpoint::new(crate::channel::HangupCause::UserBusy);
886        let ep: Endpoint = inner.into();
887        assert_eq!(ep, Endpoint::Error(inner));
888    }
889}