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