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