Skip to main content

freeswitch_types/commands/endpoint/
loopback.rs

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