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