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