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