freeswitch_types/commands/
bridge.rs1use std::fmt;
7use std::str::FromStr;
8
9use super::endpoint::Endpoint;
10use super::find_matching_bracket;
11use super::originate::OriginateError;
12use super::variables::{DialStringCarrier, Variables};
13
14const CARRIER: DialStringCarrier = DialStringCarrier::Dialplan;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct BridgeDialString {
31 #[cfg_attr(
32 feature = "serde",
33 serde(default, skip_serializing_if = "Option::is_none")
34 )]
35 variables: Option<Variables>,
36 groups: Vec<Vec<Endpoint>>,
37}
38
39impl BridgeDialString {
40 pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
42 Self {
43 variables: None,
44 groups,
45 }
46 }
47
48 pub fn with_variables(mut self, variables: Variables) -> Self {
50 self.variables = Some(variables);
51 self
52 }
53
54 pub fn variables(&self) -> Option<&Variables> {
56 self.variables
57 .as_ref()
58 }
59
60 pub fn groups(&self) -> &[Vec<Endpoint>] {
63 &self.groups
64 }
65
66 pub fn variables_mut(&mut self) -> &mut Option<Variables> {
68 &mut self.variables
69 }
70
71 pub fn groups_mut(&mut self) -> &mut Vec<Vec<Endpoint>> {
73 &mut self.groups
74 }
75}
76
77impl fmt::Display for BridgeDialString {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 if let Some(vars) = &self.variables {
80 if !vars.is_empty() {
81 write!(f, "{}", vars.display_for(CARRIER))?;
82 }
83 }
84 for (gi, group) in self
85 .groups
86 .iter()
87 .enumerate()
88 {
89 if gi > 0 {
90 f.write_str("|")?;
91 }
92 for (ei, ep) in group
93 .iter()
94 .enumerate()
95 {
96 if ei > 0 {
97 f.write_str(",")?;
98 }
99 write!(f, "{}", ep.display_for(CARRIER))?;
100 }
101 }
102 Ok(())
103 }
104}
105
106impl FromStr for BridgeDialString {
107 type Err = OriginateError;
108
109 fn from_str(s: &str) -> Result<Self, Self::Err> {
110 let s = s.trim();
111 if s.is_empty() {
112 return Err(OriginateError::ParseError(
113 "empty bridge dial string".into(),
114 ));
115 }
116
117 let (variables, rest) = if s.starts_with('{') {
119 let close = find_matching_bracket(s, '{', '}').ok_or_else(|| {
120 OriginateError::ParseError("unclosed { in bridge dial string".into())
121 })?;
122 let var_str = &s[..=close];
123 let vars = Variables::parse_for(var_str, CARRIER)?;
124 let vars = if vars.is_empty() { None } else { Some(vars) };
125 (vars, &s[close + 1..])
126 } else {
127 (None, s)
128 };
129
130 let group_strs = split_respecting_brackets(rest, '|');
132 let mut groups = Vec::new();
133 for group_str in &group_strs {
134 let group_str = group_str.trim();
135 if group_str.is_empty() {
136 continue;
137 }
138 let ep_strs = split_respecting_brackets(group_str, ',');
140 let mut endpoints = Vec::new();
141 for ep_str in &ep_strs {
142 let ep_str = ep_str.trim();
143 if ep_str.is_empty() {
144 continue;
145 }
146 let ep = Endpoint::parse_for(ep_str, CARRIER)?;
147 endpoints.push(ep);
148 }
149 if !endpoints.is_empty() {
150 groups.push(endpoints);
151 }
152 }
153
154 Ok(Self { variables, groups })
155 }
156}
157
158fn split_respecting_brackets(s: &str, sep: char) -> Vec<&str> {
161 let mut parts = Vec::new();
162 let mut depth = 0i32;
163 let mut start = 0;
164 let bytes = s.as_bytes();
165
166 for (i, &b) in bytes
167 .iter()
168 .enumerate()
169 {
170 match b {
171 b'{' | b'[' | b'<' | b'(' => depth += 1,
172 b'}' | b']' | b'>' | b')' => {
173 depth -= 1;
174 if depth < 0 {
175 depth = 0;
176 }
177 }
178 _ if b == sep as u8 && depth == 0 => {
179 parts.push(&s[start..i]);
180 start = i + 1;
181 }
182 _ => {}
183 }
184 }
185 parts.push(&s[start..]);
186 parts
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::commands::endpoint::{ErrorEndpoint, LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
193 use crate::commands::variables::VariablesType;
194
195 #[test]
201 fn bridge_renders_one_level_shallower_than_the_default() {
202 let mut vars = Variables::new(VariablesType::Default);
203 vars.insert("cid", "it's");
204 let ep = SofiaGateway::new("gw", "18005551234");
205 let bridge = BridgeDialString::new(vec![vec![ep.into()]]).with_variables(vars.clone());
206
207 assert_eq!(
208 bridge.to_string(),
209 r"{cid=it\\'s}sofia/gateway/gw/18005551234"
210 );
211 assert_eq!(vars.to_string(), r"{cid=it\\\'s}");
212 }
213
214 #[test]
218 fn per_endpoint_quoted_values_round_trip() {
219 let mut ep_vars = Variables::new(VariablesType::Channel);
220 ep_vars.insert("cid", "it's");
221 ep_vars.insert("other", "don't");
222 let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
223 .with_variables(ep_vars)
224 .into()]]);
225
226 let rendered = bridge.to_string();
227 let back: BridgeDialString = rendered
228 .parse()
229 .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
230 assert_eq!(back, bridge, "rendered {rendered}");
231 }
232
233 #[test]
234 fn display_single_endpoint() {
235 let bridge =
236 BridgeDialString::new(vec![vec![
237 SofiaGateway::new("my_provider", "18005551234").into()
238 ]]);
239 assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
240 }
241
242 #[test]
243 fn display_simultaneous_ring() {
244 let bridge = BridgeDialString::new(vec![vec![
245 SofiaGateway::new("primary", "18005551234").into(),
246 SofiaGateway::new("secondary", "18005551234").into(),
247 ]]);
248 assert_eq!(
249 bridge.to_string(),
250 "sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
251 );
252 }
253
254 #[test]
255 fn display_sequential_failover() {
256 let bridge = BridgeDialString::new(vec![
257 vec![SofiaGateway::new("primary", "18005551234").into()],
258 vec![SofiaGateway::new("backup", "18005551234").into()],
259 ]);
260 assert_eq!(
261 bridge.to_string(),
262 "sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
263 );
264 }
265
266 #[test]
267 fn display_mixed_simultaneous_and_sequential() {
268 let bridge = BridgeDialString::new(vec![
269 vec![
270 SofiaGateway::new("primary", "1234").into(),
271 SofiaGateway::new("secondary", "1234").into(),
272 ],
273 vec![SofiaGateway::new("backup", "1234").into()],
274 ]);
275 assert_eq!(
276 bridge.to_string(),
277 "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
278 );
279 }
280
281 #[test]
282 fn display_with_global_variables() {
283 let mut vars = Variables::new(VariablesType::Default);
284 vars.insert("hangup_after_bridge", "true");
285 let bridge =
286 BridgeDialString::new(vec![vec![
287 SofiaEndpoint::new("internal", "1000@domain").into()
288 ]])
289 .with_variables(vars);
290 assert_eq!(
291 bridge.to_string(),
292 "{hangup_after_bridge=true}sofia/internal/1000@domain"
293 );
294 }
295
296 #[test]
297 fn display_with_per_endpoint_variables() {
298 let mut ep_vars = Variables::new(VariablesType::Channel);
299 ep_vars.insert("leg_timeout", "30");
300 let bridge = BridgeDialString::new(vec![vec![
301 SofiaGateway::new("gw1", "1234")
302 .with_variables(ep_vars)
303 .into(),
304 SofiaGateway::new("gw2", "1234").into(),
305 ]]);
306 assert_eq!(
307 bridge.to_string(),
308 "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
309 );
310 }
311
312 #[test]
313 fn display_with_error_endpoint_failover() {
314 let bridge = BridgeDialString::new(vec![
315 vec![SofiaGateway::new("primary", "1234").into()],
316 vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
317 ]);
318 assert_eq!(
319 bridge.to_string(),
320 "sofia/gateway/primary/1234|error/USER_BUSY"
321 );
322 }
323
324 #[test]
325 fn display_with_loopback() {
326 let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
327 .with_context("default")
328 .into()]]);
329 assert_eq!(bridge.to_string(), "loopback/9199/default");
330 }
331
332 #[test]
335 fn from_str_single_endpoint() {
336 let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
337 .parse()
338 .unwrap();
339 assert_eq!(
340 bridge
341 .groups()
342 .len(),
343 1
344 );
345 assert_eq!(bridge.groups()[0].len(), 1);
346 assert!(bridge
347 .variables()
348 .is_none());
349 }
350
351 #[test]
352 fn from_str_simultaneous_ring() {
353 let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/1234"
354 .parse()
355 .unwrap();
356 assert_eq!(
357 bridge
358 .groups()
359 .len(),
360 1
361 );
362 assert_eq!(bridge.groups()[0].len(), 2);
363 }
364
365 #[test]
366 fn from_str_sequential_failover() {
367 let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
368 .parse()
369 .unwrap();
370 assert_eq!(
371 bridge
372 .groups()
373 .len(),
374 2
375 );
376 assert_eq!(bridge.groups()[0].len(), 1);
377 assert_eq!(bridge.groups()[1].len(), 1);
378 }
379
380 #[test]
381 fn from_str_mixed() {
382 let bridge: BridgeDialString =
383 "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
384 .parse()
385 .unwrap();
386 assert_eq!(
387 bridge
388 .groups()
389 .len(),
390 2
391 );
392 assert_eq!(bridge.groups()[0].len(), 2);
393 assert_eq!(bridge.groups()[1].len(), 1);
394 }
395
396 #[test]
397 fn from_str_with_global_variables() {
398 let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
399 .parse()
400 .unwrap();
401 assert!(bridge
402 .variables()
403 .is_some());
404 assert_eq!(
405 bridge
406 .variables()
407 .unwrap()
408 .get("hangup_after_bridge"),
409 Some("true")
410 );
411 assert_eq!(
412 bridge
413 .groups()
414 .len(),
415 1
416 );
417 assert_eq!(bridge.groups()[0].len(), 1);
418 }
419
420 #[test]
421 fn from_str_with_per_endpoint_variables() {
422 let bridge: BridgeDialString =
423 "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
424 .parse()
425 .unwrap();
426 assert_eq!(
427 bridge
428 .groups()
429 .len(),
430 1
431 );
432 assert_eq!(bridge.groups()[0].len(), 2);
433 let ep = &bridge.groups()[0][0];
434 if let Endpoint::SofiaGateway(gw) = ep {
435 assert!(gw
436 .variables
437 .is_some());
438 } else {
439 panic!("expected SofiaGateway");
440 }
441 }
442
443 #[test]
444 fn from_str_round_trip_single() {
445 let input = "sofia/gateway/my_provider/18005551234";
446 let bridge: BridgeDialString = input
447 .parse()
448 .unwrap();
449 assert_eq!(bridge.to_string(), input);
450 }
451
452 #[test]
453 fn from_str_round_trip_mixed() {
454 let input =
455 "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
456 let bridge: BridgeDialString = input
457 .parse()
458 .unwrap();
459 assert_eq!(bridge.to_string(), input);
460 }
461
462 #[test]
463 fn from_str_round_trip_with_global_vars() {
464 let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
465 let bridge: BridgeDialString = input
466 .parse()
467 .unwrap();
468 assert_eq!(bridge.to_string(), input);
469 }
470
471 #[test]
474 fn serde_round_trip_single() {
475 let bridge =
476 BridgeDialString::new(vec![vec![
477 SofiaGateway::new("my_provider", "18005551234").into()
478 ]]);
479 let json = serde_json::to_string(&bridge).unwrap();
480 let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
481 assert_eq!(bridge, parsed);
482 }
483
484 #[test]
485 fn serde_round_trip_multi_group() {
486 let mut vars = Variables::new(VariablesType::Default);
487 vars.insert("hangup_after_bridge", "true");
488 let bridge = BridgeDialString::new(vec![
489 vec![
490 SofiaGateway::new("primary", "1234").into(),
491 SofiaGateway::new("secondary", "1234").into(),
492 ],
493 vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
494 ])
495 .with_variables(vars);
496 let json = serde_json::to_string(&bridge).unwrap();
497 let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
498 assert_eq!(bridge, parsed);
499 }
500
501 #[test]
504 fn from_str_empty_string_rejected() {
505 let result = "".parse::<BridgeDialString>();
506 assert!(result.is_err());
507 }
508
509 #[test]
510 fn from_str_whitespace_only_rejected() {
511 let result = " ".parse::<BridgeDialString>();
512 assert!(result.is_err());
513 }
514
515 #[test]
516 fn from_str_empty_groups_from_trailing_pipe() {
517 let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
519 .parse()
520 .unwrap();
521 assert_eq!(
522 bridge
523 .groups()
524 .len(),
525 1
526 );
527 }
528
529 #[test]
530 fn from_str_empty_variable_block() {
531 let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
532 .parse()
533 .unwrap();
534 assert!(bridge
535 .variables()
536 .is_none());
537 assert_eq!(
538 bridge
539 .groups()
540 .len(),
541 1
542 );
543 }
544
545 #[test]
546 fn from_str_mismatched_bracket_rejected() {
547 let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
548 assert!(result.is_err());
549 }
550
551 #[test]
552 fn serde_to_display_wire_format() {
553 let json = r#"{
554 "groups": [[{
555 "sofia_gateway": {
556 "gateway": "my_gw",
557 "destination": "18005551234"
558 }
559 }]]
560 }"#;
561 let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
562 assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
563 }
564}