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