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::{BlockParse, DialStringCarrier, DialStringTarget, 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
80/// Renders a [`BridgeDialString`] for one parser revision. Returned by
81/// [`BridgeDialString::display_with`].
82#[derive(Debug, Clone, Copy)]
83pub struct BridgeDialStringDisplay<'a> {
84    bridge: &'a BridgeDialString,
85    block_parse: BlockParse,
86}
87
88impl fmt::Display for BridgeDialStringDisplay<'_> {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        self.bridge
91            .write_with(f, self.block_parse)
92    }
93}
94
95impl fmt::Display for BridgeDialString {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        self.write_with(f, BlockParse::default())
98    }
99}
100
101impl FromStr for BridgeDialString {
102    type Err = OriginateError;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        Self::parse_with(s, BlockParse::default())
106    }
107}
108
109impl BridgeDialString {
110    /// Render for a switch running `block_parse`, rather than the default
111    /// revision [`Display`](fmt::Display) uses.
112    pub fn display_with(&self, block_parse: BlockParse) -> BridgeDialStringDisplay<'_> {
113        BridgeDialStringDisplay {
114            bridge: self,
115            block_parse,
116        }
117    }
118
119    fn write_with(&self, f: &mut fmt::Formatter<'_>, block_parse: BlockParse) -> fmt::Result {
120        let target = DialStringTarget::new(CARRIER).with_block_parse(block_parse);
121        if let Some(vars) = &self.variables {
122            if !vars.is_empty() {
123                write!(f, "{}", vars.display_for(target))?;
124            }
125        }
126        for (gi, group) in self
127            .groups
128            .iter()
129            .enumerate()
130        {
131            if gi > 0 {
132                f.write_str("|")?;
133            }
134            for (ei, ep) in group
135                .iter()
136                .enumerate()
137            {
138                if ei > 0 {
139                    f.write_str(",")?;
140                }
141                write!(f, "{}", ep.display_for(target))?;
142            }
143        }
144        Ok(())
145    }
146
147    /// Parse a dial string written for a switch running `block_parse`, mirroring
148    /// [`display_with`](Self::display_with).
149    pub fn parse_with(s: &str, block_parse: BlockParse) -> Result<Self, OriginateError> {
150        let target = DialStringTarget::new(CARRIER).with_block_parse(block_parse);
151        let s = s.trim();
152        if s.is_empty() {
153            return Err(OriginateError::ParseError(
154                "empty bridge dial string".into(),
155            ));
156        }
157
158        let (variables, rest) = extract_scoped_variables(s, target, GLOBAL_SCOPES)?;
159
160        // Split on | for sequential groups, respecting brackets
161        let group_strs = split_respecting_brackets(rest, '|');
162        let mut groups = Vec::new();
163        for group_str in &group_strs {
164            let group_str = group_str.trim();
165            if group_str.is_empty() {
166                continue;
167            }
168            // Split on , for simultaneous endpoints, respecting brackets
169            let ep_strs = split_respecting_brackets(group_str, ',');
170            let mut endpoints = Vec::new();
171            for ep_str in &ep_strs {
172                let ep_str = ep_str.trim();
173                if ep_str.is_empty() {
174                    continue;
175                }
176                let ep = Endpoint::parse_for(ep_str, CARRIER)?;
177                endpoints.push(ep);
178            }
179            if !endpoints.is_empty() {
180                groups.push(endpoints);
181            }
182        }
183
184        Ok(Self { variables, groups })
185    }
186}
187
188/// Split a string on `sep` while skipping separators inside `{...}`, `[...]`,
189/// `<...>`, and `${...}` blocks.
190fn split_respecting_brackets(s: &str, sep: char) -> Vec<&str> {
191    let mut parts = Vec::new();
192    let mut depth = 0i32;
193    let mut start = 0;
194    let bytes = s.as_bytes();
195
196    for (i, &b) in bytes
197        .iter()
198        .enumerate()
199    {
200        match b {
201            b'{' | b'[' | b'<' | b'(' => depth += 1,
202            b'}' | b']' | b'>' | b')' => {
203                depth -= 1;
204                if depth < 0 {
205                    depth = 0;
206                }
207            }
208            _ if b == sep as u8 && depth == 0 => {
209                parts.push(&s[start..i]);
210                start = i + 1;
211            }
212            _ => {}
213        }
214    }
215    parts.push(&s[start..]);
216    parts
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::commands::endpoint::{
223        DialString, ErrorEndpoint, LoopbackEndpoint, SofiaEndpoint, SofiaGateway,
224    };
225    use crate::commands::variables::{BlockParse, VariablesType};
226
227    // === Display ===
228
229    /// `Display` and `FromStr` are the default revision, not a second render path.
230    #[test]
231    fn display_and_from_str_are_the_default_revision() {
232        let mut vars = Variables::new(VariablesType::Default);
233        vars.insert("cid", "it's");
234        let mut ep_vars = Variables::new(VariablesType::Channel);
235        ep_vars.insert("path", r"C:\path");
236        let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
237            .with_variables(ep_vars)
238            .into()]])
239        .with_variables(vars);
240
241        let rendered = bridge
242            .display_with(BlockParse::PairSplitCleans)
243            .to_string();
244        assert_eq!(rendered, bridge.to_string());
245        assert_eq!(
246            BridgeDialString::parse_with(&rendered, BlockParse::PairSplitCleans)
247                .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}")),
248            bridge
249        );
250    }
251
252    /// The block reaches a dialplan application whole, one tokenizer pass
253    /// shallower than an `originate` argument, so a quoted value carries one
254    /// backslash fewer here than the same value rendered on its own.
255    #[test]
256    fn bridge_renders_one_level_shallower_than_the_default() {
257        let mut vars = Variables::new(VariablesType::Default);
258        vars.insert("cid", "it's");
259        let ep = SofiaGateway::new("gw", "18005551234");
260        let bridge = BridgeDialString::new(vec![vec![ep.into()]]).with_variables(vars.clone());
261
262        assert_eq!(
263            bridge.to_string(),
264            r"{cid=it\\\\\\'s}sofia/gateway/gw/18005551234"
265        );
266        assert_eq!(vars.to_string(), r"{cid=it\\\\\\\'s}");
267    }
268
269    /// Both halves have to agree on the carrier. Rendering at dialplan depth
270    /// and parsing back at the default would unescape one level too deep and
271    /// silently return a different value than was put in.
272    #[test]
273    fn per_endpoint_escaped_values_round_trip() {
274        let mut ep_vars = Variables::new(VariablesType::Channel);
275        ep_vars.insert("path", r"C:\path");
276        ep_vars.insert("other", "a,b");
277        let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
278            .with_variables(ep_vars)
279            .into()]]);
280
281        let rendered = bridge.to_string();
282        let back: BridgeDialString = rendered
283            .parse()
284            .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
285        assert_eq!(back, bridge, "rendered {rendered}");
286    }
287
288    /// A quote in a `[]` block pairs across values before the switch parses the
289    /// block, at any escaping depth, so the parser refuses one rather than hand
290    /// back a value the wire would not deliver.
291    #[test]
292    fn per_endpoint_quoted_value_is_refused() {
293        let err = r"[cid=it\\\\\\'s]sofia/gateway/gw/1234"
294            .parse::<BridgeDialString>()
295            .unwrap_err()
296            .to_string();
297        assert!(
298            err.contains("cid") && err.contains("channel scope"),
299            "error does not name the variable and scope: {err}"
300        );
301    }
302
303    #[test]
304    fn display_single_endpoint() {
305        let bridge =
306            BridgeDialString::new(vec![vec![
307                SofiaGateway::new("my_provider", "18005551234").into()
308            ]]);
309        assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
310    }
311
312    #[test]
313    fn display_simultaneous_ring() {
314        let bridge = BridgeDialString::new(vec![vec![
315            SofiaGateway::new("primary", "18005551234").into(),
316            SofiaGateway::new("secondary", "18005551234").into(),
317        ]]);
318        assert_eq!(
319            bridge.to_string(),
320            "sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
321        );
322    }
323
324    #[test]
325    fn display_sequential_failover() {
326        let bridge = BridgeDialString::new(vec![
327            vec![SofiaGateway::new("primary", "18005551234").into()],
328            vec![SofiaGateway::new("backup", "18005551234").into()],
329        ]);
330        assert_eq!(
331            bridge.to_string(),
332            "sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
333        );
334    }
335
336    #[test]
337    fn display_mixed_simultaneous_and_sequential() {
338        let bridge = BridgeDialString::new(vec![
339            vec![
340                SofiaGateway::new("primary", "1234").into(),
341                SofiaGateway::new("secondary", "1234").into(),
342            ],
343            vec![SofiaGateway::new("backup", "1234").into()],
344        ]);
345        assert_eq!(
346            bridge.to_string(),
347            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
348        );
349    }
350
351    #[test]
352    fn display_with_global_variables() {
353        let mut vars = Variables::new(VariablesType::Default);
354        vars.insert("hangup_after_bridge", "true");
355        let bridge =
356            BridgeDialString::new(vec![vec![
357                SofiaEndpoint::new("internal", "1000@domain").into()
358            ]])
359            .with_variables(vars);
360        assert_eq!(
361            bridge.to_string(),
362            "{hangup_after_bridge=true}sofia/internal/1000@domain"
363        );
364    }
365
366    #[test]
367    fn display_with_per_endpoint_variables() {
368        let mut ep_vars = Variables::new(VariablesType::Channel);
369        ep_vars.insert("leg_timeout", "30");
370        let bridge = BridgeDialString::new(vec![vec![
371            SofiaGateway::new("gw1", "1234")
372                .with_variables(ep_vars)
373                .into(),
374            SofiaGateway::new("gw2", "1234").into(),
375        ]]);
376        assert_eq!(
377            bridge.to_string(),
378            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
379        );
380    }
381
382    #[test]
383    fn display_with_error_endpoint_failover() {
384        let bridge = BridgeDialString::new(vec![
385            vec![SofiaGateway::new("primary", "1234").into()],
386            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
387        ]);
388        assert_eq!(
389            bridge.to_string(),
390            "sofia/gateway/primary/1234|error/USER_BUSY"
391        );
392    }
393
394    #[test]
395    fn display_with_loopback() {
396        let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
397            .with_context("default")
398            .into()]]);
399        assert_eq!(bridge.to_string(), "loopback/9199/default");
400    }
401
402    // === FromStr ===
403
404    #[test]
405    fn from_str_single_endpoint() {
406        let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
407            .parse()
408            .unwrap();
409        assert_eq!(
410            bridge
411                .groups()
412                .len(),
413            1
414        );
415        assert_eq!(bridge.groups()[0].len(), 1);
416        assert!(bridge
417            .variables()
418            .is_none());
419    }
420
421    #[test]
422    fn from_str_simultaneous_ring() {
423        let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/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    }
434
435    #[test]
436    fn from_str_sequential_failover() {
437        let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
438            .parse()
439            .unwrap();
440        assert_eq!(
441            bridge
442                .groups()
443                .len(),
444            2
445        );
446        assert_eq!(bridge.groups()[0].len(), 1);
447        assert_eq!(bridge.groups()[1].len(), 1);
448    }
449
450    #[test]
451    fn from_str_mixed() {
452        let bridge: BridgeDialString =
453            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
454                .parse()
455                .unwrap();
456        assert_eq!(
457            bridge
458                .groups()
459                .len(),
460            2
461        );
462        assert_eq!(bridge.groups()[0].len(), 2);
463        assert_eq!(bridge.groups()[1].len(), 1);
464    }
465
466    #[test]
467    fn from_str_with_global_variables() {
468        let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
469            .parse()
470            .unwrap();
471        assert!(bridge
472            .variables()
473            .is_some());
474        assert_eq!(
475            bridge
476                .variables()
477                .unwrap()
478                .get("hangup_after_bridge"),
479            Some("true")
480        );
481        assert_eq!(
482            bridge
483                .groups()
484                .len(),
485            1
486        );
487        assert_eq!(bridge.groups()[0].len(), 1);
488    }
489
490    #[test]
491    fn from_str_with_per_endpoint_variables() {
492        let bridge: BridgeDialString =
493            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
494                .parse()
495                .unwrap();
496        assert_eq!(
497            bridge
498                .groups()
499                .len(),
500            1
501        );
502        assert_eq!(bridge.groups()[0].len(), 2);
503        let ep = &bridge.groups()[0][0];
504        if let Endpoint::SofiaGateway(gw) = ep {
505            assert!(gw
506                .variables
507                .is_some());
508        } else {
509            panic!("expected SofiaGateway");
510        }
511    }
512
513    /// An enterprise block is global too. Read as part of the first endpoint it
514    /// either fails the parse or lands on one leg of a forked dial.
515    #[test]
516    fn from_str_with_enterprise_global_variables() {
517        let bridge: BridgeDialString = "<originate_timeout=60>sofia/internal/1000@domain"
518            .parse()
519            .unwrap();
520        assert_eq!(
521            bridge
522                .variables()
523                .expect("enterprise block is global")
524                .get("originate_timeout"),
525            Some("60")
526        );
527        assert_eq!(bridge.groups()[0].len(), 1);
528    }
529
530    /// A channel block binds to the endpoint that follows it, so the global
531    /// slot must not take it.
532    #[test]
533    fn from_str_leaves_a_channel_block_on_its_endpoint() {
534        let bridge: BridgeDialString = "[leg_timeout=30]sofia/gateway/gw1/1234"
535            .parse()
536            .unwrap();
537        assert!(bridge
538            .variables()
539            .is_none());
540        assert!(bridge.groups()[0][0]
541            .variables()
542            .is_some());
543    }
544
545    #[test]
546    fn from_str_round_trip_single() {
547        let input = "sofia/gateway/my_provider/18005551234";
548        let bridge: BridgeDialString = input
549            .parse()
550            .unwrap();
551        assert_eq!(bridge.to_string(), input);
552    }
553
554    #[test]
555    fn from_str_round_trip_mixed() {
556        let input =
557            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
558        let bridge: BridgeDialString = input
559            .parse()
560            .unwrap();
561        assert_eq!(bridge.to_string(), input);
562    }
563
564    #[test]
565    fn from_str_round_trip_with_global_vars() {
566        let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
567        let bridge: BridgeDialString = input
568            .parse()
569            .unwrap();
570        assert_eq!(bridge.to_string(), input);
571    }
572
573    // === Serde ===
574
575    #[test]
576    fn serde_round_trip_single() {
577        let bridge =
578            BridgeDialString::new(vec![vec![
579                SofiaGateway::new("my_provider", "18005551234").into()
580            ]]);
581        let json = serde_json::to_string(&bridge).unwrap();
582        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
583        assert_eq!(bridge, parsed);
584    }
585
586    #[test]
587    fn serde_round_trip_multi_group() {
588        let mut vars = Variables::new(VariablesType::Default);
589        vars.insert("hangup_after_bridge", "true");
590        let bridge = BridgeDialString::new(vec![
591            vec![
592                SofiaGateway::new("primary", "1234").into(),
593                SofiaGateway::new("secondary", "1234").into(),
594            ],
595            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
596        ])
597        .with_variables(vars);
598        let json = serde_json::to_string(&bridge).unwrap();
599        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
600        assert_eq!(bridge, parsed);
601    }
602
603    // === Edge cases ===
604
605    #[test]
606    fn from_str_empty_string_rejected() {
607        let result = "".parse::<BridgeDialString>();
608        assert!(result.is_err());
609    }
610
611    #[test]
612    fn from_str_whitespace_only_rejected() {
613        let result = "   ".parse::<BridgeDialString>();
614        assert!(result.is_err());
615    }
616
617    #[test]
618    fn from_str_empty_groups_from_trailing_pipe() {
619        // "ep1|" should parse as one group (empty trailing group is skipped)
620        let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
621            .parse()
622            .unwrap();
623        assert_eq!(
624            bridge
625                .groups()
626                .len(),
627            1
628        );
629    }
630
631    #[test]
632    fn from_str_empty_variable_block() {
633        let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
634            .parse()
635            .unwrap();
636        assert!(bridge
637            .variables()
638            .is_none());
639        assert_eq!(
640            bridge
641                .groups()
642                .len(),
643            1
644        );
645    }
646
647    #[test]
648    fn from_str_mismatched_bracket_rejected() {
649        let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
650        assert!(result.is_err());
651    }
652
653    #[test]
654    fn serde_to_display_wire_format() {
655        let json = r#"{
656            "groups": [[{
657                "sofia_gateway": {
658                    "gateway": "my_gw",
659                    "destination": "18005551234"
660                }
661            }]]
662        }"#;
663        let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
664        assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
665    }
666}