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