freeswitch_types/commands/endpoint/
loopback.rs1use std::str::FromStr;
2
3use super::strip_endpoint_prefix;
4use crate::commands::originate::{OriginateError, DEFAULT_CONTEXT};
5use crate::commands::variables::DialStringCarrier;
6use crate::commands::variables::Variables;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[non_exhaustive]
12pub struct LoopbackEndpoint {
13 pub extension: String,
15 #[cfg_attr(
18 feature = "serde",
19 serde(default, skip_serializing_if = "Option::is_none")
20 )]
21 pub context: Option<String>,
22 #[cfg_attr(
25 feature = "serde",
26 serde(default, skip_serializing_if = "Option::is_none")
27 )]
28 pub dialplan: Option<String>,
29 #[cfg_attr(
31 feature = "serde",
32 serde(default, skip_serializing_if = "Option::is_none")
33 )]
34 pub variables: Option<Variables>,
35}
36
37impl LoopbackEndpoint {
38 pub fn new(extension: impl Into<String>) -> Self {
40 Self {
41 extension: extension.into(),
42 context: None,
43 dialplan: None,
44 variables: None,
45 }
46 }
47
48 pub fn with_context(mut self, context: impl Into<String>) -> Self {
50 self.context = Some(context.into());
51 self
52 }
53
54 pub fn with_dialplan(mut self, dialplan: impl Into<String>) -> Self {
57 self.dialplan = Some(dialplan.into());
58 self
59 }
60}
61
62impl_dial_string_with_variables!(
63 LoopbackEndpoint,
64 |this, f| match (&this.context, &this.dialplan) {
65 (None, None) => write!(f, "loopback/{}", this.extension),
66 (Some(ctx), None) => write!(f, "loopback/{}/{}", this.extension, ctx),
67 (ctx, Some(dialplan)) => write!(
68 f,
69 "loopback/{}/{}/{}",
70 this.extension,
71 ctx.as_deref()
72 .unwrap_or(DEFAULT_CONTEXT),
73 dialplan
74 ),
75 }
76);
77
78impl FromStr for LoopbackEndpoint {
79 type Err = OriginateError;
80
81 fn from_str(s: &str) -> Result<Self, Self::Err> {
82 let (variables, rest) =
83 strip_endpoint_prefix(s, "loopback/", "loopback", DialStringCarrier::EslApi)?;
84 let mut segments = rest.splitn(3, '/');
85 let extension = segments
86 .next()
87 .unwrap_or(rest);
88 Ok(Self {
89 extension: extension.into(),
90 context: segments
91 .next()
92 .map(str::to_string),
93 dialplan: segments
94 .next()
95 .map(str::to_string),
96 variables,
97 })
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::commands::variables::VariablesType;
105
106 #[test]
107 fn loopback_display_no_context() {
108 let ep = LoopbackEndpoint::new("9199");
109 assert_eq!(ep.to_string(), "loopback/9199");
110 }
111
112 #[test]
113 fn loopback_display_with_context() {
114 let ep = LoopbackEndpoint::new("9199").with_context("default");
115 assert_eq!(ep.to_string(), "loopback/9199/default");
116 }
117
118 #[test]
119 fn loopback_display_with_variables() {
120 let mut vars = Variables::new(VariablesType::Default);
121 vars.insert("loopback_initial_codec", "L16@48000h");
122 let ep = LoopbackEndpoint::new("100")
123 .with_context("test")
124 .with_variables(vars);
125 assert_eq!(
126 ep.to_string(),
127 "{loopback_initial_codec=L16@48000h}loopback/100/test"
128 );
129 }
130
131 #[test]
132 fn loopback_from_str_with_context() {
133 let ep: LoopbackEndpoint = "loopback/9199/test"
134 .parse()
135 .unwrap();
136 assert_eq!(ep.extension, "9199");
137 assert_eq!(
138 ep.context
139 .as_deref(),
140 Some("test")
141 );
142 }
143
144 #[test]
145 fn loopback_from_str_no_context() {
146 let ep: LoopbackEndpoint = "loopback/9199"
147 .parse()
148 .unwrap();
149 assert_eq!(ep.extension, "9199");
150 assert!(ep
151 .context
152 .is_none());
153 }
154
155 #[test]
156 fn loopback_round_trip_with_context() {
157 let ep = LoopbackEndpoint::new("100").with_context("myctx");
158 let s = ep.to_string();
159 let parsed: LoopbackEndpoint = s
160 .parse()
161 .unwrap();
162 assert_eq!(parsed, ep);
163 }
164
165 #[test]
169 fn loopback_carries_a_dialplan_segment() {
170 let ep: LoopbackEndpoint = "loopback/9199/default/xml"
171 .parse()
172 .unwrap();
173 assert_eq!(ep.extension, "9199");
174 assert_eq!(
175 ep.context
176 .as_deref(),
177 Some("default")
178 );
179 assert_eq!(
180 ep.dialplan
181 .as_deref(),
182 Some("xml")
183 );
184 assert_eq!(ep.to_string(), "loopback/9199/default/xml");
185 }
186
187 #[test]
190 fn loopback_dialplan_without_a_context_fills_the_gap() {
191 let ep = LoopbackEndpoint::new("9199").with_dialplan("inline");
192 assert_eq!(ep.to_string(), "loopback/9199/default/inline");
193 assert_eq!(
194 ep.to_string()
195 .parse::<LoopbackEndpoint>()
196 .unwrap()
197 .dialplan
198 .as_deref(),
199 Some("inline")
200 );
201 }
202
203 #[test]
204 fn loopback_round_trip_no_context() {
205 let ep = LoopbackEndpoint::new("9199");
206 let s = ep.to_string();
207 let parsed: LoopbackEndpoint = s
208 .parse()
209 .unwrap();
210 assert_eq!(parsed, ep);
211 }
212
213 #[test]
214 fn loopback_display_parse_display_stable() {
215 let inputs = [
216 "loopback/9199",
217 "loopback/100/default",
218 "loopback/ext123/custom_ctx",
219 "loopback/9199/default/xml",
220 "loopback/100/custom_ctx/inline",
221 ];
222 for input in inputs {
223 let parsed: LoopbackEndpoint = input
224 .parse()
225 .unwrap();
226 let displayed = parsed.to_string();
227 assert_eq!(displayed, input, "round-trip failed for: {}", input);
228 let reparsed: LoopbackEndpoint = displayed
229 .parse()
230 .unwrap();
231 assert_eq!(reparsed, parsed);
232 }
233 }
234
235 #[test]
236 fn serde_loopback_endpoint() {
237 let ep = LoopbackEndpoint::new("9199").with_context("default");
238 let json = serde_json::to_string(&ep).unwrap();
239 let parsed: LoopbackEndpoint = serde_json::from_str(&json).unwrap();
240 assert_eq!(parsed, ep);
241 }
242}