Skip to main content

freeswitch_types/commands/endpoint/
loopback.rs

1use super::{after_prefix, check_field, undeliverable, EndpointFieldFault};
2use crate::commands::originate::{OriginateError, DEFAULT_CONTEXT};
3use crate::commands::variables::Variables;
4
5/// Internal loopback endpoint: `loopback/{extension}[/{context}[/{dialplan}]]`.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(try_from = "config::LoopbackEndpoint"))]
9#[non_exhaustive]
10pub struct LoopbackEndpoint {
11    /// Extension number or pattern, or `app=<name>[:<args>]`, which mod_loopback runs as an
12    /// application and follows with no context or dialplan.
13    pub extension: String,
14    /// Dialplan context. `None` omits the context segment, letting
15    /// FreeSWITCH use its default.
16    #[cfg_attr(
17        feature = "serde",
18        serde(default, skip_serializing_if = "Option::is_none")
19    )]
20    pub context: Option<String>,
21    /// Dialplan for the re-entered leg. `None` omits the segment, letting
22    /// FreeSWITCH use its default.
23    #[cfg_attr(
24        feature = "serde",
25        serde(default, skip_serializing_if = "Option::is_none")
26    )]
27    pub dialplan: Option<String>,
28    /// Per-channel variables prepended as `{key=value}`.
29    #[cfg_attr(
30        feature = "serde",
31        serde(default, skip_serializing_if = "Option::is_none")
32    )]
33    pub variables: Option<Variables>,
34}
35
36impl LoopbackEndpoint {
37    /// Create a new loopback endpoint with no explicit context.
38    pub fn new(extension: impl Into<String>) -> Self {
39        Self {
40            extension: extension.into(),
41            context: None,
42            dialplan: None,
43            variables: None,
44        }
45    }
46
47    /// Set the dialplan context.
48    pub fn with_context(mut self, context: impl Into<String>) -> Self {
49        self.context = Some(context.into());
50        self
51    }
52
53    /// Set the dialplan. It is the third positional segment, so rendering one
54    /// without a context emits [`DEFAULT_CONTEXT`] in the gap.
55    pub fn with_dialplan(mut self, dialplan: impl Into<String>) -> Self {
56        self.dialplan = Some(dialplan.into());
57        self
58    }
59}
60
61impl_dial_string_with_variables!(LoopbackEndpoint, write_module_text, |this| match (
62    &this.context,
63    &this.dialplan
64) {
65    (None, None) => format!("loopback/{}", this.extension),
66    (Some(ctx), None) => format!("loopback/{}/{}", this.extension, ctx),
67    (ctx, Some(dialplan)) => format!(
68        "loopback/{}/{}/{}",
69        this.extension,
70        ctx.as_deref()
71            .unwrap_or(DEFAULT_CONTEXT),
72        dialplan
73    ),
74});
75
76const KIND: &str = "loopback";
77
78/// `channel_outgoing_channel` runs an extension opening `app=` in any case as an application.
79fn is_application(extension: &str) -> bool {
80    extension
81        .get(..4)
82        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("app="))
83}
84
85impl LoopbackEndpoint {
86    /// Refuse what `channel_outgoing_channel` reads as something else: a `/` in the extension,
87    /// the context or an application's name, an empty context or dialplan, and either after an
88    /// application.
89    pub(crate) fn check_deliverable(&self) -> Result<(), OriginateError> {
90        if is_application(&self.extension) {
91            let name = self
92                .extension
93                .split_once(':')
94                .map_or(
95                    self.extension
96                        .as_str(),
97                    |(name, _)| name,
98                );
99            check_field(KIND, "extension", name, &["/"])?;
100            check_field(KIND, "extension", &self.extension, &[])?;
101            for (field, value) in [("context", &self.context), ("dialplan", &self.dialplan)] {
102                if value.is_some() {
103                    return Err(undeliverable(
104                        KIND,
105                        field,
106                        EndpointFieldFault::FollowsAnApplication,
107                    ));
108                }
109            }
110            return Ok(());
111        }
112        check_field(KIND, "extension", &self.extension, &["/"])?;
113        for (field, value, separators) in [
114            ("context", &self.context, &["/"][..]),
115            ("dialplan", &self.dialplan, &[][..]),
116        ] {
117            let Some(value) = value else {
118                continue;
119            };
120            check_field(KIND, field, value, separators)?;
121            if value.is_empty() {
122                return Err(undeliverable(
123                    KIND,
124                    field,
125                    EndpointFieldFault::EmptyReadsAsDefault,
126                ));
127            }
128        }
129        Ok(())
130    }
131
132    pub(crate) fn parse_bare(text: &str) -> Result<Self, OriginateError> {
133        let rest = after_prefix(text, "loopback/", KIND)?;
134        let ep = if is_application(rest) {
135            Self::new(rest)
136        } else {
137            let mut segments = rest.splitn(3, '/');
138            let mut ep = Self::new(
139                segments
140                    .next()
141                    .unwrap_or(rest),
142            );
143            ep.context = segments
144                .next()
145                .map(str::to_string);
146            ep.dialplan = segments
147                .next()
148                .map(str::to_string);
149            ep
150        };
151        ep.check_deliverable()?;
152        Ok(ep)
153    }
154}
155
156impl_endpoint_parse!(from_str: LoopbackEndpoint);
157
158impl_endpoint_parse!(config:
159    LoopbackEndpoint {
160        extension: String,
161        ;
162        context: Option<String>,
163        dialplan: Option<String>,
164        variables: Option<Variables>,
165    }
166);
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::commands::variables::VariablesType;
172
173    #[test]
174    fn loopback_display_no_context() {
175        let ep = LoopbackEndpoint::new("9199");
176        assert_eq!(ep.to_string(), "loopback/9199");
177    }
178
179    #[test]
180    fn loopback_display_with_context() {
181        let ep = LoopbackEndpoint::new("9199").with_context("default");
182        assert_eq!(ep.to_string(), "loopback/9199/default");
183    }
184
185    #[test]
186    fn loopback_display_with_variables() {
187        let mut vars = Variables::new(VariablesType::Default);
188        vars.insert("loopback_initial_codec", "L16@48000h");
189        let ep = LoopbackEndpoint::new("100")
190            .with_context("test")
191            .with_variables(vars);
192        assert_eq!(
193            ep.to_string(),
194            "{loopback_initial_codec=L16@48000h}loopback/100/test"
195        );
196    }
197
198    #[test]
199    fn loopback_from_str_with_context() {
200        let ep: LoopbackEndpoint = "loopback/9199/test"
201            .parse()
202            .unwrap();
203        assert_eq!(ep.extension, "9199");
204        assert_eq!(
205            ep.context
206                .as_deref(),
207            Some("test")
208        );
209    }
210
211    #[test]
212    fn loopback_from_str_no_context() {
213        let ep: LoopbackEndpoint = "loopback/9199"
214            .parse()
215            .unwrap();
216        assert_eq!(ep.extension, "9199");
217        assert!(ep
218            .context
219            .is_none());
220    }
221
222    #[test]
223    fn loopback_round_trip_with_context() {
224        let ep = LoopbackEndpoint::new("100").with_context("myctx");
225        let s = ep.to_string();
226        let parsed: LoopbackEndpoint = s
227            .parse()
228            .unwrap();
229        assert_eq!(parsed, ep);
230    }
231
232    /// `mod_loopback` splits the dial string into extension, context and
233    /// dialplan. Modelling two of the three folds `default/xml` into the
234    /// context, which then renders back as one segment the switch re-splits.
235    #[test]
236    fn loopback_carries_a_dialplan_segment() {
237        let ep: LoopbackEndpoint = "loopback/9199/default/xml"
238            .parse()
239            .unwrap();
240        assert_eq!(ep.extension, "9199");
241        assert_eq!(
242            ep.context
243                .as_deref(),
244            Some("default")
245        );
246        assert_eq!(
247            ep.dialplan
248                .as_deref(),
249            Some("xml")
250        );
251        assert_eq!(ep.to_string(), "loopback/9199/default/xml");
252    }
253
254    /// The dialplan is the third positional segment, so it cannot be written
255    /// without the second; the builder supplies the switch's own default.
256    #[test]
257    fn loopback_dialplan_without_a_context_fills_the_gap() {
258        let ep = LoopbackEndpoint::new("9199").with_dialplan("inline");
259        assert_eq!(ep.to_string(), "loopback/9199/default/inline");
260        assert_eq!(
261            ep.to_string()
262                .parse::<LoopbackEndpoint>()
263                .unwrap()
264                .dialplan
265                .as_deref(),
266            Some("inline")
267        );
268    }
269
270    #[test]
271    fn loopback_round_trip_no_context() {
272        let ep = LoopbackEndpoint::new("9199");
273        let s = ep.to_string();
274        let parsed: LoopbackEndpoint = s
275            .parse()
276            .unwrap();
277        assert_eq!(parsed, ep);
278    }
279
280    #[test]
281    fn loopback_display_parse_display_stable() {
282        let inputs = [
283            "loopback/9199",
284            "loopback/100/default",
285            "loopback/ext123/custom_ctx",
286            "loopback/9199/default/xml",
287            "loopback/100/custom_ctx/inline",
288        ];
289        for input in inputs {
290            let parsed: LoopbackEndpoint = input
291                .parse()
292                .unwrap();
293            let displayed = parsed.to_string();
294            assert_eq!(displayed, input, "round-trip failed for: {}", input);
295            let reparsed: LoopbackEndpoint = displayed
296                .parse()
297                .unwrap();
298            assert_eq!(reparsed, parsed);
299        }
300    }
301
302    #[test]
303    fn serde_loopback_endpoint() {
304        let ep = LoopbackEndpoint::new("9199").with_context("default");
305        let json = serde_json::to_string(&ep).unwrap();
306        let parsed: LoopbackEndpoint = serde_json::from_str(&json).unwrap();
307        assert_eq!(parsed, ep);
308    }
309}