Skip to main content

freeswitch_types/commands/endpoint/
loopback.rs

1use std::str::FromStr;
2
3use super::strip_endpoint_prefix;
4use crate::commands::originate::{OriginateError, DEFAULT_CONTEXT};
5use crate::commands::variables::DialStringCarrier;
6use crate::commands::variables::Variables;
7
8/// Internal loopback endpoint: `loopback/{extension}[/{context}[/{dialplan}]]`.
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[non_exhaustive]
12pub struct LoopbackEndpoint {
13    /// Extension number or pattern.
14    pub extension: String,
15    /// Dialplan context. `None` omits the context segment, letting
16    /// FreeSWITCH use its default.
17    #[cfg_attr(
18        feature = "serde",
19        serde(default, skip_serializing_if = "Option::is_none")
20    )]
21    pub context: Option<String>,
22    /// Dialplan for the re-entered leg. `None` omits the segment, letting
23    /// FreeSWITCH use its default.
24    #[cfg_attr(
25        feature = "serde",
26        serde(default, skip_serializing_if = "Option::is_none")
27    )]
28    pub dialplan: Option<String>,
29    /// Per-channel variables prepended as `{key=value}`.
30    #[cfg_attr(
31        feature = "serde",
32        serde(default, skip_serializing_if = "Option::is_none")
33    )]
34    pub variables: Option<Variables>,
35}
36
37impl LoopbackEndpoint {
38    /// Create a new loopback endpoint with no explicit context.
39    pub fn new(extension: impl Into<String>) -> Self {
40        Self {
41            extension: extension.into(),
42            context: None,
43            dialplan: None,
44            variables: None,
45        }
46    }
47
48    /// Set the dialplan context.
49    pub fn with_context(mut self, context: impl Into<String>) -> Self {
50        self.context = Some(context.into());
51        self
52    }
53
54    /// Set the dialplan. It is the third positional segment, so rendering one
55    /// without a context emits [`DEFAULT_CONTEXT`] in the gap.
56    pub fn with_dialplan(mut self, dialplan: impl Into<String>) -> Self {
57        self.dialplan = Some(dialplan.into());
58        self
59    }
60}
61
62impl_dial_string_with_variables!(
63    LoopbackEndpoint,
64    |this, f| match (&this.context, &this.dialplan) {
65        (None, None) => write!(f, "loopback/{}", this.extension),
66        (Some(ctx), None) => write!(f, "loopback/{}/{}", this.extension, ctx),
67        (ctx, Some(dialplan)) => write!(
68            f,
69            "loopback/{}/{}/{}",
70            this.extension,
71            ctx.as_deref()
72                .unwrap_or(DEFAULT_CONTEXT),
73            dialplan
74        ),
75    }
76);
77
78impl FromStr for LoopbackEndpoint {
79    type Err = OriginateError;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        let (variables, rest) =
83            strip_endpoint_prefix(s, "loopback/", "loopback", DialStringCarrier::EslApi)?;
84        let mut segments = rest.splitn(3, '/');
85        let extension = segments
86            .next()
87            .unwrap_or(rest);
88        Ok(Self {
89            extension: extension.into(),
90            context: segments
91                .next()
92                .map(str::to_string),
93            dialplan: segments
94                .next()
95                .map(str::to_string),
96            variables,
97        })
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::commands::variables::VariablesType;
105
106    #[test]
107    fn loopback_display_no_context() {
108        let ep = LoopbackEndpoint::new("9199");
109        assert_eq!(ep.to_string(), "loopback/9199");
110    }
111
112    #[test]
113    fn loopback_display_with_context() {
114        let ep = LoopbackEndpoint::new("9199").with_context("default");
115        assert_eq!(ep.to_string(), "loopback/9199/default");
116    }
117
118    #[test]
119    fn loopback_display_with_variables() {
120        let mut vars = Variables::new(VariablesType::Default);
121        vars.insert("loopback_initial_codec", "L16@48000h");
122        let ep = LoopbackEndpoint::new("100")
123            .with_context("test")
124            .with_variables(vars);
125        assert_eq!(
126            ep.to_string(),
127            "{loopback_initial_codec=L16@48000h}loopback/100/test"
128        );
129    }
130
131    #[test]
132    fn loopback_from_str_with_context() {
133        let ep: LoopbackEndpoint = "loopback/9199/test"
134            .parse()
135            .unwrap();
136        assert_eq!(ep.extension, "9199");
137        assert_eq!(
138            ep.context
139                .as_deref(),
140            Some("test")
141        );
142    }
143
144    #[test]
145    fn loopback_from_str_no_context() {
146        let ep: LoopbackEndpoint = "loopback/9199"
147            .parse()
148            .unwrap();
149        assert_eq!(ep.extension, "9199");
150        assert!(ep
151            .context
152            .is_none());
153    }
154
155    #[test]
156    fn loopback_round_trip_with_context() {
157        let ep = LoopbackEndpoint::new("100").with_context("myctx");
158        let s = ep.to_string();
159        let parsed: LoopbackEndpoint = s
160            .parse()
161            .unwrap();
162        assert_eq!(parsed, ep);
163    }
164
165    /// `mod_loopback` splits the dial string into extension, context and
166    /// dialplan. Modelling two of the three folds `default/xml` into the
167    /// context, which then renders back as one segment the switch re-splits.
168    #[test]
169    fn loopback_carries_a_dialplan_segment() {
170        let ep: LoopbackEndpoint = "loopback/9199/default/xml"
171            .parse()
172            .unwrap();
173        assert_eq!(ep.extension, "9199");
174        assert_eq!(
175            ep.context
176                .as_deref(),
177            Some("default")
178        );
179        assert_eq!(
180            ep.dialplan
181                .as_deref(),
182            Some("xml")
183        );
184        assert_eq!(ep.to_string(), "loopback/9199/default/xml");
185    }
186
187    /// The dialplan is the third positional segment, so it cannot be written
188    /// without the second; the builder supplies the switch's own default.
189    #[test]
190    fn loopback_dialplan_without_a_context_fills_the_gap() {
191        let ep = LoopbackEndpoint::new("9199").with_dialplan("inline");
192        assert_eq!(ep.to_string(), "loopback/9199/default/inline");
193        assert_eq!(
194            ep.to_string()
195                .parse::<LoopbackEndpoint>()
196                .unwrap()
197                .dialplan
198                .as_deref(),
199            Some("inline")
200        );
201    }
202
203    #[test]
204    fn loopback_round_trip_no_context() {
205        let ep = LoopbackEndpoint::new("9199");
206        let s = ep.to_string();
207        let parsed: LoopbackEndpoint = s
208            .parse()
209            .unwrap();
210        assert_eq!(parsed, ep);
211    }
212
213    #[test]
214    fn loopback_display_parse_display_stable() {
215        let inputs = [
216            "loopback/9199",
217            "loopback/100/default",
218            "loopback/ext123/custom_ctx",
219            "loopback/9199/default/xml",
220            "loopback/100/custom_ctx/inline",
221        ];
222        for input in inputs {
223            let parsed: LoopbackEndpoint = input
224                .parse()
225                .unwrap();
226            let displayed = parsed.to_string();
227            assert_eq!(displayed, input, "round-trip failed for: {}", input);
228            let reparsed: LoopbackEndpoint = displayed
229                .parse()
230                .unwrap();
231            assert_eq!(reparsed, parsed);
232        }
233    }
234
235    #[test]
236    fn serde_loopback_endpoint() {
237        let ep = LoopbackEndpoint::new("9199").with_context("default");
238        let json = serde_json::to_string(&ep).unwrap();
239        let parsed: LoopbackEndpoint = serde_json::from_str(&json).unwrap();
240        assert_eq!(parsed, ep);
241    }
242}