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