Skip to main content

freeswitch_types/commands/endpoint/
audio.rs

1use std::fmt;
2
3use super::{strip_endpoint_prefix, write_variables};
4use crate::commands::originate::OriginateError;
5use crate::commands::variables::DialStringCarrier;
6use crate::commands::variables::Variables;
7
8/// Audio device endpoint for portaudio, pulseaudio, or ALSA modules.
9///
10/// Wire format: `{module}[/{destination}]` where destination is typically
11/// empty or `auto_answer` (recognized by portaudio and pulseaudio).
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[non_exhaustive]
15pub struct AudioEndpoint {
16    /// Destination string (e.g. `auto_answer`). `None` for bare module name.
17    #[cfg_attr(
18        feature = "serde",
19        serde(default, skip_serializing_if = "Option::is_none")
20    )]
21    pub destination: Option<String>,
22    /// Per-channel variables prepended as `{key=value}`.
23    #[cfg_attr(
24        feature = "serde",
25        serde(default, skip_serializing_if = "Option::is_none")
26    )]
27    pub variables: Option<Variables>,
28}
29
30impl AudioEndpoint {
31    /// Create a new audio endpoint with no destination.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Set the destination string.
37    pub fn with_destination(mut self, destination: impl Into<String>) -> Self {
38        self.destination = Some(destination.into());
39        self
40    }
41
42    /// Format with the given module prefix (`portaudio`, `pulseaudio`, `alsa`),
43    /// which the [`Endpoint`](super::Endpoint) variant supplies because this
44    /// struct does not record which of the three modules it belongs to.
45    ///
46    /// Renders for [`DialStringCarrier::EslApi`]; the [`Endpoint`](super::Endpoint)
47    /// variants render for whichever carrier they were asked for.
48    pub fn fmt_with_prefix(&self, f: &mut fmt::Formatter<'_>, prefix: &str) -> fmt::Result {
49        self.write_with_prefix(f, prefix, DialStringCarrier::EslApi)
50    }
51
52    pub(super) fn write_with_prefix(
53        &self,
54        f: &mut fmt::Formatter<'_>,
55        prefix: &str,
56        carrier: DialStringCarrier,
57    ) -> fmt::Result {
58        write_variables(f, &self.variables, carrier)?;
59        match &self.destination {
60            Some(dest) => write!(f, "{}/{}", prefix, dest),
61            None => f.write_str(prefix),
62        }
63    }
64
65    /// Parse from a dial string with the given module prefix.
66    pub fn parse_with_prefix(s: &str, prefix: &str) -> Result<Self, OriginateError> {
67        let (variables, rest) =
68            strip_endpoint_prefix(s, prefix, prefix, DialStringCarrier::EslApi)?;
69        let destination = rest
70            .strip_prefix('/')
71            .filter(|d| !d.is_empty())
72            .map(str::to_string);
73        Ok(Self {
74            destination,
75            variables,
76        })
77    }
78}
79
80impl_dial_string_with_variables!(AudioEndpoint);
81
82/// **Warning:** This `Display` impl exists only to satisfy the `DialString: Display`
83/// trait bound. The `"audio"` prefix is not a valid FreeSWITCH endpoint.
84/// Always use `AudioEndpoint` through [`Endpoint::PortAudio`](super::Endpoint::PortAudio),
85/// [`Endpoint::PulseAudio`](super::Endpoint::PulseAudio), or
86/// [`Endpoint::Alsa`](super::Endpoint::Alsa) which call
87/// [`fmt_with_prefix`](AudioEndpoint::fmt_with_prefix) with the correct module name.
88impl fmt::Display for AudioEndpoint {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        self.fmt_with_prefix(f, "audio")
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn parse_with_prefix_portaudio() {
100        let ep = AudioEndpoint::parse_with_prefix("portaudio/auto_answer", "portaudio").unwrap();
101        assert_eq!(
102            ep.destination
103                .as_deref(),
104            Some("auto_answer")
105        );
106        assert!(ep
107            .variables
108            .is_none());
109    }
110
111    #[test]
112    fn parse_with_prefix_bare() {
113        let ep = AudioEndpoint::parse_with_prefix("alsa", "alsa").unwrap();
114        assert!(ep
115            .destination
116            .is_none());
117    }
118
119    #[test]
120    fn parse_with_prefix_trailing_slash() {
121        let ep = AudioEndpoint::parse_with_prefix("pulseaudio/", "pulseaudio").unwrap();
122        assert!(ep
123            .destination
124            .is_none());
125    }
126
127    #[test]
128    fn parse_with_prefix_wrong_module() {
129        let result = AudioEndpoint::parse_with_prefix("portaudio/x", "alsa");
130        assert!(result.is_err());
131    }
132
133    /// A module name is a whole path segment. Accepting a longer one and
134    /// keeping the prefix drops the rest of the dial string without a word.
135    #[test]
136    fn parse_with_prefix_rejects_a_longer_module_name() {
137        assert!(AudioEndpoint::parse_with_prefix("alsafoo/bar", "alsa").is_err());
138        assert!("alsafoo/bar"
139            .parse::<super::super::Endpoint>()
140            .is_err());
141    }
142}