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::{DialString, Endpoint};
10use super::originate::OriginateError;
11use super::variables::{
12    installed_variables, read_error, BlockParse, DialStringCarrier, DialStringTarget, Variables,
13};
14use crate::switch_passes::brackets::Block;
15use crate::switch_passes::originate_legs::{Leg, MAX_PEERS};
16use crate::switch_passes::pipeline;
17
18/// A bridge dial string is the argument of a dialplan application, which
19/// receives it whole, so it renders and parses one escaping level shallower
20/// than the [`DialStringCarrier::EslApi`] default the endpoint types use on
21/// their own.
22const CARRIER: DialStringCarrier = DialStringCarrier::Dialplan;
23
24/// Typed bridge dial string.
25///
26/// Format: `{global_vars}[ep1_vars]ep1,[ep2_vars]ep2|[ep3_vars]ep3`
27///
28/// - `,` separates endpoints rung simultaneously (within a group)
29/// - `|` separates groups tried sequentially (failover)
30/// - Each endpoint may have channel-scope `[variables]`
31/// - Global `{variables}` apply to all endpoints
32#[derive(Debug, Clone, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "serde", serde(try_from = "config::BridgeDialString"))]
35pub struct BridgeDialString {
36    #[cfg_attr(
37        feature = "serde",
38        serde(default, skip_serializing_if = "Option::is_none")
39    )]
40    variables: Option<Variables>,
41    groups: Vec<Vec<Endpoint>>,
42}
43
44impl BridgeDialString {
45    /// Create a new bridge dial string with the given failover groups.
46    pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
47        Self {
48            variables: None,
49            groups,
50        }
51    }
52
53    /// Set global default-scope variables.
54    pub fn with_variables(mut self, variables: Variables) -> Self {
55        self.variables = Some(variables);
56        self
57    }
58
59    /// Default-scope variables applied to all endpoints.
60    pub fn variables(&self) -> Option<&Variables> {
61        self.variables
62            .as_ref()
63    }
64
65    /// Sequential failover groups (`|`-separated). Within each group,
66    /// endpoints ring simultaneously (`,`-separated).
67    pub fn groups(&self) -> &[Vec<Endpoint>] {
68        &self.groups
69    }
70
71    /// Mutable reference to the default-scope variables.
72    pub fn variables_mut(&mut self) -> &mut Option<Variables> {
73        &mut self.variables
74    }
75
76    /// Mutable reference to the failover groups.
77    pub fn groups_mut(&mut self) -> &mut Vec<Vec<Endpoint>> {
78        &mut self.groups
79    }
80}
81
82/// Renders a [`BridgeDialString`] for one parser revision. Returned by
83/// [`BridgeDialString::display_with`].
84#[derive(Debug, Clone, Copy)]
85pub struct BridgeDialStringDisplay<'a> {
86    bridge: &'a BridgeDialString,
87    block_parse: BlockParse,
88}
89
90impl fmt::Display for BridgeDialStringDisplay<'_> {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        self.bridge
93            .write_with(f, self.block_parse)
94    }
95}
96
97impl fmt::Display for BridgeDialString {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        self.write_with(f, BlockParse::default())
100    }
101}
102
103impl FromStr for BridgeDialString {
104    type Err = OriginateError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        Self::parse_with(s, BlockParse::default())
108    }
109}
110
111impl BridgeDialString {
112    /// Render for a switch running `block_parse`, rather than the default
113    /// revision [`Display`](fmt::Display) uses.
114    pub fn display_with(&self, block_parse: BlockParse) -> BridgeDialStringDisplay<'_> {
115        BridgeDialStringDisplay {
116            bridge: self,
117            block_parse,
118        }
119    }
120
121    fn write_with(&self, f: &mut fmt::Formatter<'_>, block_parse: BlockParse) -> fmt::Result {
122        let target = DialStringTarget::new(CARRIER).with_block_parse(block_parse);
123        if let Some(vars) = &self.variables {
124            if !vars.is_empty() {
125                write!(f, "{}", vars.display_for(target))?;
126            }
127        }
128        for (gi, group) in self
129            .groups
130            .iter()
131            .enumerate()
132        {
133            if gi > 0 {
134                f.write_str("|")?;
135            }
136            for (ei, ep) in group
137                .iter()
138                .enumerate()
139            {
140                if ei > 0 {
141                    f.write_str(",")?;
142                }
143                write!(f, "{}", ep.display_for(target))?;
144            }
145        }
146        Ok(())
147    }
148
149    /// Parse a dial string written for a switch running `block_parse`, mirroring
150    /// [`display_with`](Self::display_with).
151    pub fn parse_with(s: &str, block_parse: BlockParse) -> Result<Self, OriginateError> {
152        let target = DialStringTarget::new(CARRIER).with_block_parse(block_parse);
153        let s = s.trim_matches(' ');
154        if s.is_empty() {
155            return Err(OriginateError::ParseError(
156                "empty bridge dial string".into(),
157            ));
158        }
159
160        let list = pipeline::read(s, target).map_err(read_error)?;
161        let [thread] = &list.threads[..] else {
162            return Err(OriginateError::ParseError(
163                "a bridge dial string has no enterprise threads".into(),
164            ));
165        };
166        if !list
167            .blocks
168            .is_empty()
169            || thread
170                .blocks
171                .len()
172                > 1
173        {
174            return Err(OriginateError::ParseError(
175                "a bridge dial string carries one leading variable block".into(),
176            ));
177        }
178        let variables = installed_variables(
179            thread
180                .blocks
181                .first(),
182        )?;
183
184        let past_limit = |group| {
185            list.past_limit
186                .iter()
187                .any(|cut| cut.group == group)
188        };
189        if past_limit(None) {
190            return Err(OriginateError::TooManyLegs {
191                group: None,
192                max: MAX_PEERS,
193            });
194        }
195        let mut groups = Vec::new();
196        for (k, group) in thread
197            .groups
198            .iter()
199            .enumerate()
200        {
201            if past_limit(Some(k)) {
202                return Err(OriginateError::TooManyLegs {
203                    group: Some(groups.len()),
204                    max: MAX_PEERS,
205                });
206            }
207            let legs: Vec<&Leg> = group
208                .iter()
209                .filter(|leg| {
210                    !leg.blocks
211                        .is_empty()
212                        || !leg
213                            .endpoint
214                            .is_empty()
215                })
216                .collect();
217            let texts: Option<Vec<&str>> = legs
218                .iter()
219                .map(|leg| {
220                    s.get(
221                        leg.raw
222                            .clone(),
223                    )
224                })
225                .collect();
226            if let Some(texts) = texts {
227                let joined = legs
228                    .iter()
229                    .map(|leg| reading(leg))
230                    .collect();
231                if legs_interfere(texts, Some(joined), target) {
232                    return Err(OriginateError::BracketSpansLegs {
233                        group: groups.len(),
234                    });
235                }
236            }
237            let mut endpoints = Vec::new();
238            for leg in legs {
239                if leg
240                    .blocks
241                    .len()
242                    > 1
243                {
244                    return Err(OriginateError::ParseError(
245                        "an endpoint carries one variable block".into(),
246                    ));
247                }
248                let vars = installed_variables(
249                    leg.blocks
250                        .first(),
251                )?;
252                let mut endpoint = Endpoint::parse_bare(&leg.endpoint)?;
253                if vars.is_some() {
254                    endpoint.set_variables(vars);
255                    if endpoint
256                        .variables()
257                        .is_none()
258                    {
259                        return Err(OriginateError::VariablesNotSupported(endpoint.kind()));
260                    }
261                }
262                endpoints.push(endpoint);
263            }
264            if !endpoints.is_empty() {
265                groups.push(endpoints);
266            }
267        }
268
269        let bridge = Self { variables, groups };
270        bridge.check_legs(block_parse)?;
271        Ok(bridge)
272    }
273
274    /// Refuse more legs or groups than the switch splits off, then a group whose legs the
275    /// switch's comma scan reads across.
276    fn check_legs(&self, block_parse: BlockParse) -> Result<(), OriginateError> {
277        let too_many = |group| OriginateError::TooManyLegs {
278            group,
279            max: MAX_PEERS,
280        };
281        if self
282            .groups
283            .len()
284            > MAX_PEERS
285        {
286            return Err(too_many(None));
287        }
288        if let Some(group) = self
289            .groups
290            .iter()
291            .position(|group| group.len() > MAX_PEERS)
292        {
293            return Err(too_many(Some(group)));
294        }
295        let target = DialStringTarget::new(CARRIER).with_block_parse(block_parse);
296        match self
297            .groups
298            .iter()
299            .position(|group| endpoints_interfere(group, target))
300        {
301            Some(group) => Err(OriginateError::BracketSpansLegs { group }),
302            None => Ok(()),
303        }
304    }
305}
306
307/// What the switch installs on a leg and the endpoint text it dials.
308type LegReading = (Vec<Block>, String);
309
310fn reading(leg: &Leg) -> LegReading {
311    (
312        leg.blocks
313            .clone(),
314        leg.endpoint
315            .clone(),
316    )
317}
318
319/// Each leg the switch reads of `text`, as one group.
320fn read_group(text: &str, target: DialStringTarget) -> Option<Vec<LegReading>> {
321    let list = pipeline::read(text, target).ok()?;
322    let [thread] = &list.threads[..] else {
323        return None;
324    };
325    let [group] = &thread.groups[..] else {
326        return None;
327    };
328    Some(
329        group
330            .iter()
331            .map(reading)
332            .collect(),
333    )
334}
335
336/// Whether the switch reads a group otherwise than each of its leg `texts` dialled alone, as a
337/// bracket range `switch_ivr_originate`'s comma scan runs from one leg into another makes it.
338fn legs_interfere<'a>(
339    texts: impl IntoIterator<Item = &'a str>,
340    joined: Option<Vec<LegReading>>,
341    target: DialStringTarget,
342) -> bool {
343    let Some(alone) = texts
344        .into_iter()
345        .map(|text| read_group(text, target))
346        .collect::<Option<Vec<_>>>()
347    else {
348        return false;
349    };
350    joined != Some(alone.concat())
351}
352
353/// [`legs_interfere`] for a group's endpoints as they render.
354fn endpoints_interfere(group: &[Endpoint], target: DialStringTarget) -> bool {
355    let rendered: Vec<String> = group
356        .iter()
357        .map(|endpoint| {
358            endpoint
359                .display_for(target)
360                .to_string()
361        })
362        .collect();
363    let joined = read_group(&rendered.join(","), target);
364    legs_interfere(
365        rendered
366            .iter()
367            .map(String::as_str),
368        joined,
369        target,
370    )
371}
372
373#[cfg(feature = "serde")]
374mod config {
375    use crate::commands::endpoint::Endpoint;
376    use crate::commands::variables::Variables;
377
378    #[derive(serde::Deserialize)]
379    pub(super) struct BridgeDialString {
380        #[serde(default)]
381        pub(super) variables: Option<Variables>,
382        pub(super) groups: Vec<Vec<Endpoint>>,
383    }
384}
385
386#[cfg(feature = "serde")]
387impl TryFrom<config::BridgeDialString> for BridgeDialString {
388    type Error = OriginateError;
389
390    fn try_from(config: config::BridgeDialString) -> Result<Self, Self::Error> {
391        let bridge = Self {
392            variables: config.variables,
393            groups: config.groups,
394        };
395        bridge.check_legs(BlockParse::default())?;
396        Ok(bridge)
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::commands::endpoint::{
404        AudioEndpoint, DialString, ErrorEndpoint, LoopbackEndpoint, SofiaEndpoint, SofiaGateway,
405    };
406    use crate::commands::variables::{BlockParse, VariablesType};
407
408    /// The comma scan runs after the application's argument is expanded, which here unescapes the
409    /// quote that kept the `[` and `]` apart, so the switch dials both endpoints as one leg.
410    #[cfg(feature = "serde")]
411    #[test]
412    fn a_bracket_spanning_legs_after_expansion_is_refused() {
413        let bridge = BridgeDialString::new(vec![vec![
414            crate::commands::endpoint::UserEndpoint::new("['").into(),
415            SofiaGateway::new("", "]")
416                .with_profile(" ")
417                .into(),
418        ]]);
419        let json = serde_json::to_value(&bridge).unwrap();
420        assert!(matches!(
421            serde_json::from_value::<BridgeDialString>(json)
422                .map_err(|e| e.to_string()),
423            Err(e) if e.contains("group 0")
424        ));
425    }
426
427    // === Display ===
428
429    /// `Display` and `FromStr` are the default revision, not a second render path.
430    #[test]
431    fn display_and_from_str_are_the_default_revision() {
432        let mut vars = Variables::new(VariablesType::Default);
433        vars.insert("cid", "it's");
434        let mut ep_vars = Variables::new(VariablesType::Channel);
435        ep_vars.insert("path", r"C:\path");
436        let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
437            .with_variables(ep_vars)
438            .into()]])
439        .with_variables(vars);
440
441        let rendered = bridge
442            .display_with(BlockParse::PairSplitCleans)
443            .to_string();
444        assert_eq!(rendered, bridge.to_string());
445        assert_eq!(
446            BridgeDialString::parse_with(&rendered, BlockParse::PairSplitCleans)
447                .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}")),
448            bridge
449        );
450    }
451
452    /// The block reaches a dialplan application whole, one tokenizer pass
453    /// shallower than an `originate` argument, so a quoted value carries one
454    /// backslash fewer here than the same value rendered on its own.
455    #[test]
456    fn bridge_renders_one_level_shallower_than_the_default() {
457        let mut vars = Variables::new(VariablesType::Default);
458        vars.insert("cid", "it's");
459        let ep = SofiaGateway::new("gw", "18005551234");
460        let bridge = BridgeDialString::new(vec![vec![ep.into()]]).with_variables(vars.clone());
461
462        assert_eq!(
463            bridge.to_string(),
464            r"{cid=it\\\\\\'s}sofia/gateway/gw/18005551234"
465        );
466        assert_eq!(vars.to_string(), r"{cid=it\\\\\\\'s}");
467    }
468
469    /// Both halves have to agree on the carrier. Rendering at dialplan depth
470    /// and parsing back at the default would unescape one level too deep and
471    /// silently return a different value than was put in.
472    #[test]
473    fn per_endpoint_escaped_values_round_trip() {
474        let mut ep_vars = Variables::new(VariablesType::Channel);
475        ep_vars.insert("path", r"C:\path");
476        ep_vars.insert("other", "a,b");
477        let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
478            .with_variables(ep_vars)
479            .into()]]);
480
481        let rendered = bridge.to_string();
482        let back: BridgeDialString = rendered
483            .parse()
484            .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
485        assert_eq!(back, bridge, "rendered {rendered}");
486    }
487
488    /// A quote in a `[]` block pairs across values before the switch parses the
489    /// block, at any escaping depth, so the parser refuses one rather than hand
490    /// back a value the wire would not deliver.
491    #[test]
492    fn per_endpoint_quoted_value_is_refused() {
493        let err = r"[cid=it\\\\\\'s]sofia/gateway/gw/1234"
494            .parse::<BridgeDialString>()
495            .unwrap_err()
496            .to_string();
497        assert!(
498            err.contains("cid") && err.contains("channel scope"),
499            "error does not name the variable and scope: {err}"
500        );
501    }
502
503    #[test]
504    fn display_single_endpoint() {
505        let bridge =
506            BridgeDialString::new(vec![vec![
507                SofiaGateway::new("my_provider", "18005551234").into()
508            ]]);
509        assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
510    }
511
512    #[test]
513    fn display_simultaneous_ring() {
514        let bridge = BridgeDialString::new(vec![vec![
515            SofiaGateway::new("primary", "18005551234").into(),
516            SofiaGateway::new("secondary", "18005551234").into(),
517        ]]);
518        assert_eq!(
519            bridge.to_string(),
520            "sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
521        );
522    }
523
524    #[test]
525    fn display_sequential_failover() {
526        let bridge = BridgeDialString::new(vec![
527            vec![SofiaGateway::new("primary", "18005551234").into()],
528            vec![SofiaGateway::new("backup", "18005551234").into()],
529        ]);
530        assert_eq!(
531            bridge.to_string(),
532            "sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
533        );
534    }
535
536    #[test]
537    fn display_mixed_simultaneous_and_sequential() {
538        let bridge = BridgeDialString::new(vec![
539            vec![
540                SofiaGateway::new("primary", "1234").into(),
541                SofiaGateway::new("secondary", "1234").into(),
542            ],
543            vec![SofiaGateway::new("backup", "1234").into()],
544        ]);
545        assert_eq!(
546            bridge.to_string(),
547            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
548        );
549    }
550
551    #[test]
552    fn display_with_global_variables() {
553        let mut vars = Variables::new(VariablesType::Default);
554        vars.insert("hangup_after_bridge", "true");
555        let bridge =
556            BridgeDialString::new(vec![vec![
557                SofiaEndpoint::new("internal", "1000@domain").into()
558            ]])
559            .with_variables(vars);
560        assert_eq!(
561            bridge.to_string(),
562            "{hangup_after_bridge=true}sofia/internal/1000@domain"
563        );
564    }
565
566    #[test]
567    fn display_with_per_endpoint_variables() {
568        let mut ep_vars = Variables::new(VariablesType::Channel);
569        ep_vars.insert("leg_timeout", "30");
570        let bridge = BridgeDialString::new(vec![vec![
571            SofiaGateway::new("gw1", "1234")
572                .with_variables(ep_vars)
573                .into(),
574            SofiaGateway::new("gw2", "1234").into(),
575        ]]);
576        assert_eq!(
577            bridge.to_string(),
578            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
579        );
580    }
581
582    #[test]
583    fn display_with_error_endpoint_failover() {
584        let bridge = BridgeDialString::new(vec![
585            vec![SofiaGateway::new("primary", "1234").into()],
586            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
587        ]);
588        assert_eq!(
589            bridge.to_string(),
590            "sofia/gateway/primary/1234|error/USER_BUSY"
591        );
592    }
593
594    #[test]
595    fn display_with_loopback() {
596        let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
597            .with_context("default")
598            .into()]]);
599        assert_eq!(bridge.to_string(), "loopback/9199/default");
600    }
601
602    // === FromStr ===
603
604    #[test]
605    fn from_str_single_endpoint() {
606        let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
607            .parse()
608            .unwrap();
609        assert_eq!(
610            bridge
611                .groups()
612                .len(),
613            1
614        );
615        assert_eq!(bridge.groups()[0].len(), 1);
616        assert!(bridge
617            .variables()
618            .is_none());
619    }
620
621    #[test]
622    fn from_str_simultaneous_ring() {
623        let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/1234"
624            .parse()
625            .unwrap();
626        assert_eq!(
627            bridge
628                .groups()
629                .len(),
630            1
631        );
632        assert_eq!(bridge.groups()[0].len(), 2);
633    }
634
635    #[test]
636    fn from_str_sequential_failover() {
637        let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
638            .parse()
639            .unwrap();
640        assert_eq!(
641            bridge
642                .groups()
643                .len(),
644            2
645        );
646        assert_eq!(bridge.groups()[0].len(), 1);
647        assert_eq!(bridge.groups()[1].len(), 1);
648    }
649
650    #[test]
651    fn from_str_mixed() {
652        let bridge: BridgeDialString =
653            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
654                .parse()
655                .unwrap();
656        assert_eq!(
657            bridge
658                .groups()
659                .len(),
660            2
661        );
662        assert_eq!(bridge.groups()[0].len(), 2);
663        assert_eq!(bridge.groups()[1].len(), 1);
664    }
665
666    #[test]
667    fn from_str_with_global_variables() {
668        let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
669            .parse()
670            .unwrap();
671        assert!(bridge
672            .variables()
673            .is_some());
674        assert_eq!(
675            bridge
676                .variables()
677                .unwrap()
678                .get("hangup_after_bridge"),
679            Some("true")
680        );
681        assert_eq!(
682            bridge
683                .groups()
684                .len(),
685            1
686        );
687        assert_eq!(bridge.groups()[0].len(), 1);
688    }
689
690    #[test]
691    fn from_str_with_per_endpoint_variables() {
692        let bridge: BridgeDialString =
693            "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
694                .parse()
695                .unwrap();
696        assert_eq!(
697            bridge
698                .groups()
699                .len(),
700            1
701        );
702        assert_eq!(bridge.groups()[0].len(), 2);
703        let ep = &bridge.groups()[0][0];
704        if let Endpoint::SofiaGateway(gw) = ep {
705            assert!(gw
706                .variables
707                .is_some());
708        } else {
709            panic!("expected SofiaGateway");
710        }
711    }
712
713    /// An enterprise block is global too. Read as part of the first endpoint it
714    /// either fails the parse or lands on one leg of a forked dial.
715    #[test]
716    fn from_str_with_enterprise_global_variables() {
717        let bridge: BridgeDialString = "<originate_timeout=60>sofia/internal/1000@domain"
718            .parse()
719            .unwrap();
720        assert_eq!(
721            bridge
722                .variables()
723                .expect("enterprise block is global")
724                .get("originate_timeout"),
725            Some("60")
726        );
727        assert_eq!(bridge.groups()[0].len(), 1);
728    }
729
730    /// A channel block binds to the endpoint that follows it, so the global
731    /// slot must not take it.
732    #[test]
733    fn from_str_leaves_a_channel_block_on_its_endpoint() {
734        let bridge: BridgeDialString = "[leg_timeout=30]sofia/gateway/gw1/1234"
735            .parse()
736            .unwrap();
737        assert!(bridge
738            .variables()
739            .is_none());
740        assert!(bridge.groups()[0][0]
741            .variables()
742            .is_some());
743    }
744
745    #[test]
746    fn from_str_round_trip_single() {
747        let input = "sofia/gateway/my_provider/18005551234";
748        let bridge: BridgeDialString = input
749            .parse()
750            .unwrap();
751        assert_eq!(bridge.to_string(), input);
752    }
753
754    #[test]
755    fn from_str_round_trip_mixed() {
756        let input =
757            "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
758        let bridge: BridgeDialString = input
759            .parse()
760            .unwrap();
761        assert_eq!(bridge.to_string(), input);
762    }
763
764    #[test]
765    fn from_str_round_trip_with_global_vars() {
766        let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
767        let bridge: BridgeDialString = input
768            .parse()
769            .unwrap();
770        assert_eq!(bridge.to_string(), input);
771    }
772
773    // === Serde ===
774
775    #[test]
776    fn serde_round_trip_single() {
777        let bridge =
778            BridgeDialString::new(vec![vec![
779                SofiaGateway::new("my_provider", "18005551234").into()
780            ]]);
781        let json = serde_json::to_string(&bridge).unwrap();
782        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
783        assert_eq!(bridge, parsed);
784    }
785
786    #[test]
787    fn serde_round_trip_multi_group() {
788        let mut vars = Variables::new(VariablesType::Default);
789        vars.insert("hangup_after_bridge", "true");
790        let bridge = BridgeDialString::new(vec![
791            vec![
792                SofiaGateway::new("primary", "1234").into(),
793                SofiaGateway::new("secondary", "1234").into(),
794            ],
795            vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
796        ])
797        .with_variables(vars);
798        let json = serde_json::to_string(&bridge).unwrap();
799        let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
800        assert_eq!(bridge, parsed);
801    }
802
803    // === Edge cases ===
804
805    #[test]
806    fn from_str_empty_string_rejected() {
807        let result = "".parse::<BridgeDialString>();
808        assert!(result.is_err());
809    }
810
811    #[test]
812    fn from_str_whitespace_only_rejected() {
813        let result = "   ".parse::<BridgeDialString>();
814        assert!(result.is_err());
815    }
816
817    #[test]
818    fn from_str_empty_groups_from_trailing_pipe() {
819        // "ep1|" should parse as one group (empty trailing group is skipped)
820        let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
821            .parse()
822            .unwrap();
823        assert_eq!(
824            bridge
825                .groups()
826                .len(),
827            1
828        );
829    }
830
831    #[test]
832    fn from_str_empty_variable_block() {
833        let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
834            .parse()
835            .unwrap();
836        assert!(bridge
837            .variables()
838            .is_none());
839        assert_eq!(
840            bridge
841                .groups()
842                .len(),
843            1
844        );
845    }
846
847    #[test]
848    fn from_str_mismatched_bracket_rejected() {
849        let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
850        assert!(result.is_err());
851    }
852
853    /// A separator inside endpoint text is escaped for the leg split that would read it.
854    #[test]
855    fn a_separator_in_endpoint_text_round_trips() {
856        let bridge = BridgeDialString::new(vec![
857            vec![
858                SofiaEndpoint::new("internal", "a,b").into(),
859                SofiaEndpoint::new("internal", "c|d").into(),
860            ],
861            vec![LoopbackEndpoint::new("it's a").into()],
862        ]);
863        let rendered = bridge.to_string();
864        let back: BridgeDialString = rendered
865            .parse()
866            .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
867        assert_eq!(back, bridge, "rendered {rendered}");
868    }
869
870    /// The switch's comma scan protects commas from a `[` to its matching `]`, whichever leg
871    /// holds either, so a range crossing a leg separator merges the legs.
872    #[test]
873    fn a_bracket_spanning_legs_is_refused_at_config_load() {
874        let spanning = r#"{"groups":[[{"sofia":{"profile":"internal","destination":"x["}},{"sofia":{"profile":"internal","destination":"y]"}}]]}"#;
875        assert!(serde_json::from_str::<BridgeDialString>(spanning).is_err());
876        for fine in [
877            r#"{"groups":[[{"sofia":{"profile":"internal","destination":"x[]"}},{"sofia":{"profile":"internal","destination":"y]"}}]]}"#,
878            r#"{"groups":[[{"sofia":{"profile":"internal","destination":"x["}}],[{"sofia":{"profile":"internal","destination":"y]"}}]]}"#,
879        ] {
880            assert!(
881                serde_json::from_str::<BridgeDialString>(fine).is_ok(),
882                "{fine}"
883            );
884        }
885    }
886
887    /// A `[` in an earlier leg, its `]` in a later leg's endpoint, and a `^^` block between.
888    fn bracket_around_block(sep: char, value: &str) -> BridgeDialString {
889        let mut vars = Variables::new(VariablesType::Channel)
890            .with_separator(sep)
891            .unwrap();
892        vars.insert("v0", value);
893        BridgeDialString::new(vec![vec![
894            Endpoint::PortAudio(AudioEndpoint::new().with_destination("[\\")),
895            SofiaGateway::new("]", "")
896                .with_variables(vars)
897                .into(),
898        ]])
899    }
900
901    /// The comma scan takes the `^^,` head inside the range for a plain comma and writes the
902    /// block's own default, which the block then splits on alike.
903    #[test]
904    fn a_comma_head_the_scan_rewrites_round_trips() {
905        let bridge = bracket_around_block(',', "undef");
906        let rendered = bridge.to_string();
907        let back: BridgeDialString = rendered
908            .parse()
909            .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
910        assert_eq!(
911            back.to_string(),
912            r"portaudio/[\\\\\\\\,[v0=undef]sofia/gateway/]/"
913        );
914        let json = serde_json::to_value(&bridge).unwrap();
915        assert_eq!(
916            serde_json::from_value::<BridgeDialString>(json).unwrap(),
917            bridge
918        );
919    }
920
921    /// The range opened in the first leg decides how the scan rewrites a comma in the second
922    /// leg's `^^:` value, which then reaches the channel as the scan's placeholder.
923    #[test]
924    fn a_range_rewriting_a_later_legs_value_is_refused() {
925        let bridge = bracket_around_block(':', "a,b");
926        let rendered = bridge.to_string();
927        assert!(
928            matches!(
929                rendered.parse::<BridgeDialString>(),
930                Err(OriginateError::BracketSpansLegs { group: 0 })
931            ),
932            "{rendered}: {:?}",
933            rendered.parse::<BridgeDialString>()
934        );
935        let json = serde_json::to_value(&bridge).unwrap();
936        assert!(serde_json::from_value::<BridgeDialString>(json).is_err());
937    }
938
939    fn loopbacks(count: usize) -> Vec<Endpoint> {
940        (0..count)
941            .map(|n| LoopbackEndpoint::new(n.to_string()).into())
942            .collect()
943    }
944
945    fn joined(endpoints: &[Endpoint], separator: &str) -> String {
946        endpoints
947            .iter()
948            .map(ToString::to_string)
949            .collect::<Vec<_>>()
950            .join(separator)
951    }
952
953    /// Past `MAX_PEERS` the switch dials the rest of the text as the last leg's or group's
954    /// endpoint, which is refused as the limit, never as a bracket.
955    #[test]
956    fn a_group_past_the_leg_limit_is_refused_as_the_limit() {
957        let over = joined(&loopbacks(MAX_PEERS + 1), ",");
958        let err = over
959            .parse::<BridgeDialString>()
960            .unwrap_err();
961        assert_eq!(
962            err,
963            OriginateError::TooManyLegs {
964                group: Some(0),
965                max: MAX_PEERS
966            }
967        );
968        assert!(
969            err.to_string()
970                .contains(&MAX_PEERS.to_string()),
971            "{err}"
972        );
973        let at = joined(&loopbacks(MAX_PEERS), ",");
974        let bridge: BridgeDialString = at
975            .parse()
976            .unwrap();
977        assert_eq!(bridge.groups()[0].len(), MAX_PEERS);
978
979        let second = format!("loopback/a|{over}");
980        assert_eq!(
981            second.parse::<BridgeDialString>(),
982            Err(OriginateError::TooManyLegs {
983                group: Some(1),
984                max: MAX_PEERS
985            })
986        );
987    }
988
989    #[test]
990    fn groups_past_the_limit_are_refused_as_the_limit() {
991        let over = joined(&loopbacks(MAX_PEERS + 1), "|");
992        let err = over
993            .parse::<BridgeDialString>()
994            .unwrap_err();
995        assert_eq!(
996            err,
997            OriginateError::TooManyLegs {
998                group: None,
999                max: MAX_PEERS
1000            }
1001        );
1002        assert!(
1003            err.to_string()
1004                .contains(&MAX_PEERS.to_string()),
1005            "{err}"
1006        );
1007        let bridge: BridgeDialString = joined(&loopbacks(MAX_PEERS), "|")
1008            .parse()
1009            .unwrap();
1010        assert_eq!(
1011            bridge
1012                .groups()
1013                .len(),
1014            MAX_PEERS
1015        );
1016    }
1017
1018    #[cfg(feature = "serde")]
1019    #[test]
1020    fn a_config_past_the_leg_limit_is_refused_as_the_limit() {
1021        let load = |groups: Vec<Vec<Endpoint>>| {
1022            let json = serde_json::to_value(BridgeDialString::new(groups)).unwrap();
1023            serde_json::from_value::<BridgeDialString>(json).map_err(|e| e.to_string())
1024        };
1025        for groups in [
1026            vec![loopbacks(MAX_PEERS + 1)],
1027            vec![loopbacks(1); MAX_PEERS + 1],
1028        ] {
1029            let err = load(groups).unwrap_err();
1030            assert!(err.contains(&MAX_PEERS.to_string()), "{err}");
1031            assert!(!err.contains("bracket"), "{err}");
1032        }
1033        assert!(load(vec![loopbacks(MAX_PEERS)]).is_ok());
1034        assert!(load(vec![loopbacks(1); MAX_PEERS]).is_ok());
1035    }
1036
1037    /// The dialplan carrier's expansion, the leg splits over a `[]` block and both of a block's own
1038    /// splits each read `\\` as one backslash, and none reads `\b` or `\d`.
1039    #[test]
1040    fn parse_reads_blocks_as_the_switch_installs_them() {
1041        let bridge: BridgeDialString = r"{k=a\\\\b}[j=c\\\\\\\\d]loopback/9199/test"
1042            .parse()
1043            .unwrap_or_else(|e| panic!("{e}"));
1044        assert_eq!(
1045            bridge
1046                .variables()
1047                .and_then(|vars| vars.get("k")),
1048            Some(r"a\b")
1049        );
1050        assert_eq!(
1051            bridge.groups()[0][0]
1052                .variables()
1053                .and_then(|vars| vars.get("j")),
1054            Some(r"c\d")
1055        );
1056    }
1057
1058    #[test]
1059    fn serde_to_display_wire_format() {
1060        let json = r#"{
1061            "groups": [[{
1062                "sofia_gateway": {
1063                    "gateway": "my_gw",
1064                    "destination": "18005551234"
1065                }
1066            }]]
1067        }"#;
1068        let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
1069        assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
1070    }
1071}