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