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