Skip to main content

freeswitch_types/commands/endpoint/
loopback.rs

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