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