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 serde::{Deserialize, Serialize};
10
11use super::endpoint::Endpoint;
12use super::find_matching_bracket;
13use super::originate::{OriginateError, Variables};
14
15/// Typed bridge dial string.
16///
17/// Format: `{global_vars}[ep1_vars]ep1,[ep2_vars]ep2|[ep3_vars]ep3`
18///
19/// - `,` separates endpoints rung simultaneously (within a group)
20/// - `|` separates groups tried sequentially (failover)
21/// - Each endpoint may have channel-scope `[variables]`
22/// - Global `{variables}` apply to all endpoints
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[non_exhaustive]
25pub struct BridgeDialString {
26    /// Default-scope variables applied to all endpoints.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub variables: Option<Variables>,
29    /// Sequential failover groups (`|`-separated). Within each group,
30    /// endpoints ring simultaneously (`,`-separated).
31    pub groups: Vec<Vec<Endpoint>>,
32}
33
34impl BridgeDialString {
35    /// Create a new bridge dial string with the given failover groups.
36    pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
37        Self {
38            variables: None,
39            groups,
40        }
41    }
42
43    /// Set global default-scope variables.
44    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        // Extract leading {global_vars} if present
91        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        // Split on | for sequential groups, respecting brackets
104        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            // Split on , for simultaneous endpoints, respecting brackets
112            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
131/// Split a string on `sep` while skipping separators inside `{...}`, `[...]`,
132/// `<...>`, and `${...}` blocks.
133fn 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    // === Display ===
169
170    #[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    // === FromStr ===
346
347    #[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    // === Serde ===
486
487    #[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    // --- T5: BridgeDialString edge cases ---
535
536    #[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        // "ep1|" should parse as one group (empty trailing group is skipped)
551        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}