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;
12use super::variables::Variables;
13
14/// Typed bridge dial string.
15///
16/// Format: `{global_vars}[ep1_vars]ep1,[ep2_vars]ep2|[ep3_vars]ep3`
17///
18/// - `,` separates endpoints rung simultaneously (within a group)
19/// - `|` separates groups tried sequentially (failover)
20/// - Each endpoint may have channel-scope `[variables]`
21/// - Global `{variables}` apply to all endpoints
22#[derive(Debug, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct BridgeDialString {
25    #[cfg_attr(
26        feature = "serde",
27        serde(default, skip_serializing_if = "Option::is_none")
28    )]
29    variables: Option<Variables>,
30    groups: Vec<Vec<Endpoint>>,
31}
32
33impl BridgeDialString {
34    /// Create a new bridge dial string with the given failover groups.
35    pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
36        Self {
37            variables: None,
38            groups,
39        }
40    }
41
42    /// Set global default-scope variables.
43    pub fn with_variables(mut self, variables: Variables) -> Self {
44        self.variables = Some(variables);
45        self
46    }
47
48    /// Default-scope variables applied to all endpoints.
49    pub fn variables(&self) -> Option<&Variables> {
50        self.variables
51            .as_ref()
52    }
53
54    /// Sequential failover groups (`|`-separated). Within each group,
55    /// endpoints ring simultaneously (`,`-separated).
56    pub fn groups(&self) -> &[Vec<Endpoint>] {
57        &self.groups
58    }
59
60    /// Mutable reference to the default-scope variables.
61    pub fn variables_mut(&mut self) -> &mut Option<Variables> {
62        &mut self.variables
63    }
64
65    /// Mutable reference to the failover groups.
66    pub fn groups_mut(&mut self) -> &mut Vec<Vec<Endpoint>> {
67        &mut self.groups
68    }
69}
70
71impl fmt::Display for BridgeDialString {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        if let Some(vars) = &self.variables {
74            if !vars.is_empty() {
75                write!(f, "{}", vars)?;
76            }
77        }
78        for (gi, group) in self
79            .groups
80            .iter()
81            .enumerate()
82        {
83            if gi > 0 {
84                f.write_str("|")?;
85            }
86            for (ei, ep) in group
87                .iter()
88                .enumerate()
89            {
90                if ei > 0 {
91                    f.write_str(",")?;
92                }
93                write!(f, "{}", ep)?;
94            }
95        }
96        Ok(())
97    }
98}
99
100impl FromStr for BridgeDialString {
101    type Err = OriginateError;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        let s = s.trim();
105        if s.is_empty() {
106            return Err(OriginateError::ParseError(
107                "empty bridge dial string".into(),
108            ));
109        }
110
111        // Extract leading {global_vars} if present
112        let (variables, rest) = if s.starts_with('{') {
113            let close = find_matching_bracket(s, '{', '}').ok_or_else(|| {
114                OriginateError::ParseError("unclosed { in bridge dial string".into())
115            })?;
116            let var_str = &s[..=close];
117            let vars: Variables = var_str.parse()?;
118            let vars = if vars.is_empty() { None } else { Some(vars) };
119            (vars, &s[close + 1..])
120        } else {
121            (None, s)
122        };
123
124        // Split on | for sequential groups, respecting brackets
125        let group_strs = split_respecting_brackets(rest, '|');
126        let mut groups = Vec::new();
127        for group_str in &group_strs {
128            let group_str = group_str.trim();
129            if group_str.is_empty() {
130                continue;
131            }
132            // Split on , for simultaneous endpoints, respecting brackets
133            let ep_strs = split_respecting_brackets(group_str, ',');
134            let mut endpoints = Vec::new();
135            for ep_str in &ep_strs {
136                let ep_str = ep_str.trim();
137                if ep_str.is_empty() {
138                    continue;
139                }
140                let ep: Endpoint = ep_str.parse()?;
141                endpoints.push(ep);
142            }
143            if !endpoints.is_empty() {
144                groups.push(endpoints);
145            }
146        }
147
148        Ok(Self { variables, groups })
149    }
150}
151
152/// Split a string on `sep` while skipping separators inside `{...}`, `[...]`,
153/// `<...>`, and `${...}` blocks.
154fn split_respecting_brackets(s: &str, sep: char) -> Vec<&str> {
155    let mut parts = Vec::new();
156    let mut depth = 0i32;
157    let mut start = 0;
158    let bytes = s.as_bytes();
159
160    for (i, &b) in bytes
161        .iter()
162        .enumerate()
163    {
164        match b {
165            b'{' | b'[' | b'<' | b'(' => depth += 1,
166            b'}' | b']' | b'>' | b')' => {
167                depth -= 1;
168                if depth < 0 {
169                    depth = 0;
170                }
171            }
172            _ if b == sep as u8 && depth == 0 => {
173                parts.push(&s[start..i]);
174                start = i + 1;
175            }
176            _ => {}
177        }
178    }
179    parts.push(&s[start..]);
180    parts
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::commands::endpoint::{ErrorEndpoint, LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
187    use crate::commands::variables::VariablesType;
188
189    // === Display ===
190
191    #[test]
192    fn display_single_endpoint() {
193        let bridge =
194            BridgeDialString::new(vec![vec![
195                SofiaGateway::new("my_provider", "18005551234").into()
196            ]]);
197        assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
198    }
199
200    #[test]
201    fn display_simultaneous_ring() {
202        let bridge = BridgeDialString::new(vec![vec![
203            SofiaGateway::new("primary", "18005551234").into(),
204            SofiaGateway::new("secondary", "18005551234").into(),
205        ]]);
206        assert_eq!(
207            bridge.to_string(),
208            "sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
209        );
210    }
211
212    #[test]
213    fn display_sequential_failover() {
214        let bridge = BridgeDialString::new(vec![
215            vec![SofiaGateway::new("primary", "18005551234").into()],
216            vec![SofiaGateway::new("backup", "18005551234").into()],
217        ]);
218        assert_eq!(
219            bridge.to_string(),
220            "sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
221        );
222    }
223
224    #[test]
225    fn display_mixed_simultaneous_and_sequential() {
226        let bridge = BridgeDialString::new(vec![
227            vec![
228                SofiaGateway::new("primary", "1234").into(),
229                SofiaGateway::new("secondary", "1234").into(),
230            ],
231            vec![SofiaGateway::new("backup", "1234").into()],
232        ]);
233        assert_eq!(
234            bridge.to_string(),
235            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
236        );
237    }
238
239    #[test]
240    fn display_with_global_variables() {
241        let mut vars = Variables::new(VariablesType::Default);
242        vars.insert("hangup_after_bridge", "true");
243        let bridge =
244            BridgeDialString::new(vec![vec![
245                SofiaEndpoint::new("internal", "1000@domain").into()
246            ]])
247            .with_variables(vars);
248        assert_eq!(
249            bridge.to_string(),
250            "{hangup_after_bridge=true}sofia/internal/1000@domain"
251        );
252    }
253
254    #[test]
255    fn display_with_per_endpoint_variables() {
256        let mut ep_vars = Variables::new(VariablesType::Channel);
257        ep_vars.insert("leg_timeout", "30");
258        let bridge = BridgeDialString::new(vec![vec![
259            SofiaGateway::new("gw1", "1234")
260                .with_variables(ep_vars)
261                .into(),
262            SofiaGateway::new("gw2", "1234").into(),
263        ]]);
264        assert_eq!(
265            bridge.to_string(),
266            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
267        );
268    }
269
270    #[test]
271    fn display_with_error_endpoint_failover() {
272        let bridge = BridgeDialString::new(vec![
273            vec![SofiaGateway::new("primary", "1234").into()],
274            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
275        ]);
276        assert_eq!(
277            bridge.to_string(),
278            "sofia/gateway/primary/1234|error/USER_BUSY"
279        );
280    }
281
282    #[test]
283    fn display_with_loopback() {
284        let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
285            .with_context("default")
286            .into()]]);
287        assert_eq!(bridge.to_string(), "loopback/9199/default");
288    }
289
290    // === FromStr ===
291
292    #[test]
293    fn from_str_single_endpoint() {
294        let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
295            .parse()
296            .unwrap();
297        assert_eq!(
298            bridge
299                .groups()
300                .len(),
301            1
302        );
303        assert_eq!(bridge.groups()[0].len(), 1);
304        assert!(bridge
305            .variables()
306            .is_none());
307    }
308
309    #[test]
310    fn from_str_simultaneous_ring() {
311        let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/1234"
312            .parse()
313            .unwrap();
314        assert_eq!(
315            bridge
316                .groups()
317                .len(),
318            1
319        );
320        assert_eq!(bridge.groups()[0].len(), 2);
321    }
322
323    #[test]
324    fn from_str_sequential_failover() {
325        let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
326            .parse()
327            .unwrap();
328        assert_eq!(
329            bridge
330                .groups()
331                .len(),
332            2
333        );
334        assert_eq!(bridge.groups()[0].len(), 1);
335        assert_eq!(bridge.groups()[1].len(), 1);
336    }
337
338    #[test]
339    fn from_str_mixed() {
340        let bridge: BridgeDialString =
341            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
342                .parse()
343                .unwrap();
344        assert_eq!(
345            bridge
346                .groups()
347                .len(),
348            2
349        );
350        assert_eq!(bridge.groups()[0].len(), 2);
351        assert_eq!(bridge.groups()[1].len(), 1);
352    }
353
354    #[test]
355    fn from_str_with_global_variables() {
356        let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
357            .parse()
358            .unwrap();
359        assert!(bridge
360            .variables()
361            .is_some());
362        assert_eq!(
363            bridge
364                .variables()
365                .unwrap()
366                .get("hangup_after_bridge"),
367            Some("true")
368        );
369        assert_eq!(
370            bridge
371                .groups()
372                .len(),
373            1
374        );
375        assert_eq!(bridge.groups()[0].len(), 1);
376    }
377
378    #[test]
379    fn from_str_with_per_endpoint_variables() {
380        let bridge: BridgeDialString =
381            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
382                .parse()
383                .unwrap();
384        assert_eq!(
385            bridge
386                .groups()
387                .len(),
388            1
389        );
390        assert_eq!(bridge.groups()[0].len(), 2);
391        let ep = &bridge.groups()[0][0];
392        if let Endpoint::SofiaGateway(gw) = ep {
393            assert!(gw
394                .variables
395                .is_some());
396        } else {
397            panic!("expected SofiaGateway");
398        }
399    }
400
401    #[test]
402    fn from_str_round_trip_single() {
403        let input = "sofia/gateway/my_provider/18005551234";
404        let bridge: BridgeDialString = input
405            .parse()
406            .unwrap();
407        assert_eq!(bridge.to_string(), input);
408    }
409
410    #[test]
411    fn from_str_round_trip_mixed() {
412        let input =
413            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
414        let bridge: BridgeDialString = input
415            .parse()
416            .unwrap();
417        assert_eq!(bridge.to_string(), input);
418    }
419
420    #[test]
421    fn from_str_round_trip_with_global_vars() {
422        let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
423        let bridge: BridgeDialString = input
424            .parse()
425            .unwrap();
426        assert_eq!(bridge.to_string(), input);
427    }
428
429    // === Serde ===
430
431    #[test]
432    fn serde_round_trip_single() {
433        let bridge =
434            BridgeDialString::new(vec![vec![
435                SofiaGateway::new("my_provider", "18005551234").into()
436            ]]);
437        let json = serde_json::to_string(&bridge).unwrap();
438        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
439        assert_eq!(bridge, parsed);
440    }
441
442    #[test]
443    fn serde_round_trip_multi_group() {
444        let mut vars = Variables::new(VariablesType::Default);
445        vars.insert("hangup_after_bridge", "true");
446        let bridge = BridgeDialString::new(vec![
447            vec![
448                SofiaGateway::new("primary", "1234").into(),
449                SofiaGateway::new("secondary", "1234").into(),
450            ],
451            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
452        ])
453        .with_variables(vars);
454        let json = serde_json::to_string(&bridge).unwrap();
455        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
456        assert_eq!(bridge, parsed);
457    }
458
459    // --- T5: BridgeDialString edge cases ---
460
461    #[test]
462    fn from_str_empty_string_rejected() {
463        let result = "".parse::<BridgeDialString>();
464        assert!(result.is_err());
465    }
466
467    #[test]
468    fn from_str_whitespace_only_rejected() {
469        let result = "   ".parse::<BridgeDialString>();
470        assert!(result.is_err());
471    }
472
473    #[test]
474    fn from_str_empty_groups_from_trailing_pipe() {
475        // "ep1|" should parse as one group (empty trailing group is skipped)
476        let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
477            .parse()
478            .unwrap();
479        assert_eq!(
480            bridge
481                .groups()
482                .len(),
483            1
484        );
485    }
486
487    #[test]
488    fn from_str_empty_variable_block() {
489        let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
490            .parse()
491            .unwrap();
492        assert!(bridge
493            .variables()
494            .is_none());
495        assert_eq!(
496            bridge
497                .groups()
498                .len(),
499            1
500        );
501    }
502
503    #[test]
504    fn from_str_mismatched_bracket_rejected() {
505        let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
506        assert!(result.is_err());
507    }
508
509    #[test]
510    fn serde_to_display_wire_format() {
511        let json = r#"{
512            "groups": [[{
513                "sofia_gateway": {
514                    "gateway": "my_gw",
515                    "destination": "18005551234"
516                }
517            }]]
518        }"#;
519        let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
520        assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
521    }
522}