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_escaped_values_round_trip() {
213        let mut ep_vars = Variables::new(VariablesType::Channel);
214        ep_vars.insert("path", r"C:\path");
215        ep_vars.insert("other", "a,b");
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    /// A quote in a `[]` block pairs across values before the switch parses the
228    /// block, at any escaping depth, so the parser refuses one rather than hand
229    /// back a value the wire would not deliver.
230    #[test]
231    fn per_endpoint_quoted_value_is_refused() {
232        let err = r"[cid=it\\\\\\'s]sofia/gateway/gw/1234"
233            .parse::<BridgeDialString>()
234            .unwrap_err()
235            .to_string();
236        assert!(
237            err.contains("cid") && err.contains("channel scope"),
238            "error does not name the variable and scope: {err}"
239        );
240    }
241
242    #[test]
243    fn display_single_endpoint() {
244        let bridge =
245            BridgeDialString::new(vec![vec![
246                SofiaGateway::new("my_provider", "18005551234").into()
247            ]]);
248        assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
249    }
250
251    #[test]
252    fn display_simultaneous_ring() {
253        let bridge = BridgeDialString::new(vec![vec![
254            SofiaGateway::new("primary", "18005551234").into(),
255            SofiaGateway::new("secondary", "18005551234").into(),
256        ]]);
257        assert_eq!(
258            bridge.to_string(),
259            "sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
260        );
261    }
262
263    #[test]
264    fn display_sequential_failover() {
265        let bridge = BridgeDialString::new(vec![
266            vec![SofiaGateway::new("primary", "18005551234").into()],
267            vec![SofiaGateway::new("backup", "18005551234").into()],
268        ]);
269        assert_eq!(
270            bridge.to_string(),
271            "sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
272        );
273    }
274
275    #[test]
276    fn display_mixed_simultaneous_and_sequential() {
277        let bridge = BridgeDialString::new(vec![
278            vec![
279                SofiaGateway::new("primary", "1234").into(),
280                SofiaGateway::new("secondary", "1234").into(),
281            ],
282            vec![SofiaGateway::new("backup", "1234").into()],
283        ]);
284        assert_eq!(
285            bridge.to_string(),
286            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
287        );
288    }
289
290    #[test]
291    fn display_with_global_variables() {
292        let mut vars = Variables::new(VariablesType::Default);
293        vars.insert("hangup_after_bridge", "true");
294        let bridge =
295            BridgeDialString::new(vec![vec![
296                SofiaEndpoint::new("internal", "1000@domain").into()
297            ]])
298            .with_variables(vars);
299        assert_eq!(
300            bridge.to_string(),
301            "{hangup_after_bridge=true}sofia/internal/1000@domain"
302        );
303    }
304
305    #[test]
306    fn display_with_per_endpoint_variables() {
307        let mut ep_vars = Variables::new(VariablesType::Channel);
308        ep_vars.insert("leg_timeout", "30");
309        let bridge = BridgeDialString::new(vec![vec![
310            SofiaGateway::new("gw1", "1234")
311                .with_variables(ep_vars)
312                .into(),
313            SofiaGateway::new("gw2", "1234").into(),
314        ]]);
315        assert_eq!(
316            bridge.to_string(),
317            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
318        );
319    }
320
321    #[test]
322    fn display_with_error_endpoint_failover() {
323        let bridge = BridgeDialString::new(vec![
324            vec![SofiaGateway::new("primary", "1234").into()],
325            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
326        ]);
327        assert_eq!(
328            bridge.to_string(),
329            "sofia/gateway/primary/1234|error/USER_BUSY"
330        );
331    }
332
333    #[test]
334    fn display_with_loopback() {
335        let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
336            .with_context("default")
337            .into()]]);
338        assert_eq!(bridge.to_string(), "loopback/9199/default");
339    }
340
341    // === FromStr ===
342
343    #[test]
344    fn from_str_single_endpoint() {
345        let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
346            .parse()
347            .unwrap();
348        assert_eq!(
349            bridge
350                .groups()
351                .len(),
352            1
353        );
354        assert_eq!(bridge.groups()[0].len(), 1);
355        assert!(bridge
356            .variables()
357            .is_none());
358    }
359
360    #[test]
361    fn from_str_simultaneous_ring() {
362        let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/1234"
363            .parse()
364            .unwrap();
365        assert_eq!(
366            bridge
367                .groups()
368                .len(),
369            1
370        );
371        assert_eq!(bridge.groups()[0].len(), 2);
372    }
373
374    #[test]
375    fn from_str_sequential_failover() {
376        let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
377            .parse()
378            .unwrap();
379        assert_eq!(
380            bridge
381                .groups()
382                .len(),
383            2
384        );
385        assert_eq!(bridge.groups()[0].len(), 1);
386        assert_eq!(bridge.groups()[1].len(), 1);
387    }
388
389    #[test]
390    fn from_str_mixed() {
391        let bridge: BridgeDialString =
392            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
393                .parse()
394                .unwrap();
395        assert_eq!(
396            bridge
397                .groups()
398                .len(),
399            2
400        );
401        assert_eq!(bridge.groups()[0].len(), 2);
402        assert_eq!(bridge.groups()[1].len(), 1);
403    }
404
405    #[test]
406    fn from_str_with_global_variables() {
407        let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
408            .parse()
409            .unwrap();
410        assert!(bridge
411            .variables()
412            .is_some());
413        assert_eq!(
414            bridge
415                .variables()
416                .unwrap()
417                .get("hangup_after_bridge"),
418            Some("true")
419        );
420        assert_eq!(
421            bridge
422                .groups()
423                .len(),
424            1
425        );
426        assert_eq!(bridge.groups()[0].len(), 1);
427    }
428
429    #[test]
430    fn from_str_with_per_endpoint_variables() {
431        let bridge: BridgeDialString =
432            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
433                .parse()
434                .unwrap();
435        assert_eq!(
436            bridge
437                .groups()
438                .len(),
439            1
440        );
441        assert_eq!(bridge.groups()[0].len(), 2);
442        let ep = &bridge.groups()[0][0];
443        if let Endpoint::SofiaGateway(gw) = ep {
444            assert!(gw
445                .variables
446                .is_some());
447        } else {
448            panic!("expected SofiaGateway");
449        }
450    }
451
452    /// An enterprise block is global too. Read as part of the first endpoint it
453    /// either fails the parse or lands on one leg of a forked dial.
454    #[test]
455    fn from_str_with_enterprise_global_variables() {
456        let bridge: BridgeDialString = "<originate_timeout=60>sofia/internal/1000@domain"
457            .parse()
458            .unwrap();
459        assert_eq!(
460            bridge
461                .variables()
462                .expect("enterprise block is global")
463                .get("originate_timeout"),
464            Some("60")
465        );
466        assert_eq!(bridge.groups()[0].len(), 1);
467    }
468
469    /// A channel block binds to the endpoint that follows it, so the global
470    /// slot must not take it.
471    #[test]
472    fn from_str_leaves_a_channel_block_on_its_endpoint() {
473        let bridge: BridgeDialString = "[leg_timeout=30]sofia/gateway/gw1/1234"
474            .parse()
475            .unwrap();
476        assert!(bridge
477            .variables()
478            .is_none());
479        assert!(bridge.groups()[0][0]
480            .variables()
481            .is_some());
482    }
483
484    #[test]
485    fn from_str_round_trip_single() {
486        let input = "sofia/gateway/my_provider/18005551234";
487        let bridge: BridgeDialString = input
488            .parse()
489            .unwrap();
490        assert_eq!(bridge.to_string(), input);
491    }
492
493    #[test]
494    fn from_str_round_trip_mixed() {
495        let input =
496            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
497        let bridge: BridgeDialString = input
498            .parse()
499            .unwrap();
500        assert_eq!(bridge.to_string(), input);
501    }
502
503    #[test]
504    fn from_str_round_trip_with_global_vars() {
505        let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
506        let bridge: BridgeDialString = input
507            .parse()
508            .unwrap();
509        assert_eq!(bridge.to_string(), input);
510    }
511
512    // === Serde ===
513
514    #[test]
515    fn serde_round_trip_single() {
516        let bridge =
517            BridgeDialString::new(vec![vec![
518                SofiaGateway::new("my_provider", "18005551234").into()
519            ]]);
520        let json = serde_json::to_string(&bridge).unwrap();
521        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
522        assert_eq!(bridge, parsed);
523    }
524
525    #[test]
526    fn serde_round_trip_multi_group() {
527        let mut vars = Variables::new(VariablesType::Default);
528        vars.insert("hangup_after_bridge", "true");
529        let bridge = BridgeDialString::new(vec![
530            vec![
531                SofiaGateway::new("primary", "1234").into(),
532                SofiaGateway::new("secondary", "1234").into(),
533            ],
534            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
535        ])
536        .with_variables(vars);
537        let json = serde_json::to_string(&bridge).unwrap();
538        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
539        assert_eq!(bridge, parsed);
540    }
541
542    // === Edge cases ===
543
544    #[test]
545    fn from_str_empty_string_rejected() {
546        let result = "".parse::<BridgeDialString>();
547        assert!(result.is_err());
548    }
549
550    #[test]
551    fn from_str_whitespace_only_rejected() {
552        let result = "   ".parse::<BridgeDialString>();
553        assert!(result.is_err());
554    }
555
556    #[test]
557    fn from_str_empty_groups_from_trailing_pipe() {
558        // "ep1|" should parse as one group (empty trailing group is skipped)
559        let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
560            .parse()
561            .unwrap();
562        assert_eq!(
563            bridge
564                .groups()
565                .len(),
566            1
567        );
568    }
569
570    #[test]
571    fn from_str_empty_variable_block() {
572        let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
573            .parse()
574            .unwrap();
575        assert!(bridge
576            .variables()
577            .is_none());
578        assert_eq!(
579            bridge
580                .groups()
581                .len(),
582            1
583        );
584    }
585
586    #[test]
587    fn from_str_mismatched_bracket_rejected() {
588        let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
589        assert!(result.is_err());
590    }
591
592    #[test]
593    fn serde_to_display_wire_format() {
594        let json = r#"{
595            "groups": [[{
596                "sofia_gateway": {
597                    "gateway": "my_gw",
598                    "destination": "18005551234"
599                }
600            }]]
601        }"#;
602        let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
603        assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
604    }
605}