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