freeswitch_types/commands/endpoint/
loopback.rs1use super::{after_prefix, check_field, undeliverable, EndpointFieldFault};
2use crate::commands::originate::{OriginateError, DEFAULT_CONTEXT};
3use crate::commands::variables::Variables;
4
5#[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 pub extension: String,
14 #[cfg_attr(
17 feature = "serde",
18 serde(default, skip_serializing_if = "Option::is_none")
19 )]
20 pub context: Option<String>,
21 #[cfg_attr(
24 feature = "serde",
25 serde(default, skip_serializing_if = "Option::is_none")
26 )]
27 pub dialplan: Option<String>,
28 #[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 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 pub fn with_context(mut self, context: impl Into<String>) -> Self {
49 self.context = Some(context.into());
50 self
51 }
52
53 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
78fn 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 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 #[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 #[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}