Skip to main content

freeswitch_types/commands/
bridge.rs

1//! Bridge dial string builder for multi-endpoint bridge commands.
2//!
3//! Supports simultaneous ring (`,`) and sequential failover (`|`)
4//! with per-endpoint channel variables and global default variables.
5
6use std::fmt;
7use std::str::FromStr;
8
9use super::endpoint::Endpoint;
10use super::find_matching_bracket;
11use super::originate::{OriginateError, Variables};
12
13/// Typed bridge dial string.
14///
15/// Format: `{global_vars}[ep1_vars]ep1,[ep2_vars]ep2|[ep3_vars]ep3`
16///
17/// - `,` separates endpoints rung simultaneously (within a group)
18/// - `|` separates groups tried sequentially (failover)
19/// - Each endpoint may have channel-scope `[variables]`
20/// - Global `{variables}` apply to all endpoints
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[non_exhaustive]
24pub struct BridgeDialString {
25    /// Default-scope variables applied to all endpoints.
26    #[cfg_attr(
27        feature = "serde",
28        serde(default, skip_serializing_if = "Option::is_none")
29    )]
30    pub variables: Option<Variables>,
31    /// Sequential failover groups (`|`-separated). Within each group,
32    /// endpoints ring simultaneously (`,`-separated).
33    pub groups: Vec<Vec<Endpoint>>,
34}
35
36impl BridgeDialString {
37    /// Create a new bridge dial string with the given failover groups.
38    pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
39        Self {
40            variables: None,
41            groups,
42        }
43    }
44
45    /// Set global default-scope variables.
46    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        // Extract leading {global_vars} if present
93        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        // Split on | for sequential groups, respecting brackets
106        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            // Split on , for simultaneous endpoints, respecting brackets
114            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
133/// Split a string on `sep` while skipping separators inside `{...}`, `[...]`,
134/// `<...>`, and `${...}` blocks.
135fn 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    // === Display ===
171
172    #[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    // === FromStr ===
348
349    #[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    // === Serde ===
488
489    #[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    // --- T5: BridgeDialString edge cases ---
537
538    #[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        // "ep1|" should parse as one group (empty trailing group is skipped)
553        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}