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