Skip to main content

freeswitch_types/commands/endpoint/
loopback.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use super::{extract_variables, write_variables};
7use crate::commands::originate::{OriginateError, Variables};
8
9/// Internal loopback endpoint: `loopback/{extension}[/{context}]`.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, 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    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub context: Option<String>,
19    /// Per-channel variables prepended as `{key=value}`.
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub variables: Option<Variables>,
22}
23
24impl LoopbackEndpoint {
25    /// Create a new loopback endpoint with no explicit context.
26    pub fn new(extension: impl Into<String>) -> Self {
27        Self {
28            extension: extension.into(),
29            context: None,
30            variables: None,
31        }
32    }
33
34    /// Set the dialplan context.
35    pub fn with_context(mut self, context: impl Into<String>) -> Self {
36        self.context = Some(context.into());
37        self
38    }
39
40    /// Set per-channel variables.
41    pub fn with_variables(mut self, variables: Variables) -> Self {
42        self.variables = Some(variables);
43        self
44    }
45}
46
47impl fmt::Display for LoopbackEndpoint {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write_variables(f, &self.variables)?;
50        match &self.context {
51            Some(ctx) => write!(f, "loopback/{}/{}", self.extension, ctx),
52            None => write!(f, "loopback/{}", self.extension),
53        }
54    }
55}
56
57impl FromStr for LoopbackEndpoint {
58    type Err = OriginateError;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        let (variables, uri) = extract_variables(s)?;
62        let rest = uri
63            .strip_prefix("loopback/")
64            .ok_or_else(|| OriginateError::ParseError("not a loopback endpoint".into()))?;
65        let (extension, context) = match rest.split_once('/') {
66            Some((ext, ctx)) => (ext, Some(ctx.to_string())),
67            None => (rest, None),
68        };
69        Ok(Self {
70            extension: extension.into(),
71            context,
72            variables,
73        })
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::commands::originate::VariablesType;
81
82    #[test]
83    fn loopback_display_no_context() {
84        let ep = LoopbackEndpoint::new("9199");
85        assert_eq!(ep.to_string(), "loopback/9199");
86    }
87
88    #[test]
89    fn loopback_display_with_context() {
90        let ep = LoopbackEndpoint::new("9199").with_context("default");
91        assert_eq!(ep.to_string(), "loopback/9199/default");
92    }
93
94    #[test]
95    fn loopback_display_with_variables() {
96        let mut vars = Variables::new(VariablesType::Default);
97        vars.insert("loopback_initial_codec", "L16@48000h");
98        let ep = LoopbackEndpoint::new("100")
99            .with_context("test")
100            .with_variables(vars);
101        assert_eq!(
102            ep.to_string(),
103            "{loopback_initial_codec=L16@48000h}loopback/100/test"
104        );
105    }
106
107    #[test]
108    fn loopback_from_str_with_context() {
109        let ep: LoopbackEndpoint = "loopback/9199/test"
110            .parse()
111            .unwrap();
112        assert_eq!(ep.extension, "9199");
113        assert_eq!(
114            ep.context
115                .as_deref(),
116            Some("test")
117        );
118    }
119
120    #[test]
121    fn loopback_from_str_no_context() {
122        let ep: LoopbackEndpoint = "loopback/9199"
123            .parse()
124            .unwrap();
125        assert_eq!(ep.extension, "9199");
126        assert!(ep
127            .context
128            .is_none());
129    }
130
131    #[test]
132    fn loopback_round_trip_with_context() {
133        let ep = LoopbackEndpoint::new("100").with_context("myctx");
134        let s = ep.to_string();
135        let parsed: LoopbackEndpoint = s
136            .parse()
137            .unwrap();
138        assert_eq!(parsed, ep);
139    }
140
141    #[test]
142    fn loopback_round_trip_no_context() {
143        let ep = LoopbackEndpoint::new("9199");
144        let s = ep.to_string();
145        let parsed: LoopbackEndpoint = s
146            .parse()
147            .unwrap();
148        assert_eq!(parsed, ep);
149    }
150
151    // --- T5: LoopbackEndpoint parse -> display asymmetry ---
152    // Display omits context when None, but FromStr always produces
153    // context=None for bare "loopback/ext". Round-trip is symmetric.
154
155    #[test]
156    fn loopback_display_parse_display_stable() {
157        let inputs = [
158            "loopback/9199",
159            "loopback/100/default",
160            "loopback/ext123/custom_ctx",
161        ];
162        for input in inputs {
163            let parsed: LoopbackEndpoint = input
164                .parse()
165                .unwrap();
166            let displayed = parsed.to_string();
167            assert_eq!(displayed, input, "round-trip failed for: {}", input);
168            let reparsed: LoopbackEndpoint = displayed
169                .parse()
170                .unwrap();
171            assert_eq!(reparsed, parsed);
172        }
173    }
174
175    #[test]
176    fn serde_loopback_endpoint() {
177        let ep = LoopbackEndpoint::new("9199").with_context("default");
178        let json = serde_json::to_string(&ep).unwrap();
179        let parsed: LoopbackEndpoint = serde_json::from_str(&json).unwrap();
180        assert_eq!(parsed, ep);
181    }
182}