1use 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;
16use crate::switch_passes::pipeline;
17
18const CARRIER: DialStringCarrier = DialStringCarrier::Dialplan;
23
24#[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 pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
47 Self {
48 variables: None,
49 groups,
50 }
51 }
52
53 pub fn with_variables(mut self, variables: Variables) -> Self {
55 self.variables = Some(variables);
56 self
57 }
58
59 pub fn variables(&self) -> Option<&Variables> {
61 self.variables
62 .as_ref()
63 }
64
65 pub fn groups(&self) -> &[Vec<Endpoint>] {
68 &self.groups
69 }
70
71 pub fn variables_mut(&mut self) -> &mut Option<Variables> {
73 &mut self.variables
74 }
75
76 pub fn groups_mut(&mut self) -> &mut Vec<Vec<Endpoint>> {
78 &mut self.groups
79 }
80}
81
82#[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 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 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 mut groups = Vec::new();
185 for group in &thread.groups {
186 let legs: Vec<&Leg> = group
187 .iter()
188 .filter(|leg| {
189 !leg.blocks
190 .is_empty()
191 || !leg
192 .endpoint
193 .is_empty()
194 })
195 .collect();
196 let texts: Option<Vec<&str>> = legs
197 .iter()
198 .map(|leg| {
199 s.get(
200 leg.raw
201 .clone(),
202 )
203 })
204 .collect();
205 if let Some(texts) = texts {
206 let joined = legs
207 .iter()
208 .map(|leg| reading(leg))
209 .collect();
210 if legs_interfere(texts, Some(joined), target) {
211 return Err(OriginateError::BracketSpansLegs {
212 group: groups.len(),
213 });
214 }
215 }
216 let mut endpoints = Vec::new();
217 for leg in legs {
218 if leg
219 .blocks
220 .len()
221 > 1
222 {
223 return Err(OriginateError::ParseError(
224 "an endpoint carries one variable block".into(),
225 ));
226 }
227 let vars = installed_variables(
228 leg.blocks
229 .first(),
230 )?;
231 let mut endpoint = Endpoint::parse_bare(&leg.endpoint)?;
232 if vars.is_some() {
233 endpoint.set_variables(vars);
234 if endpoint
235 .variables()
236 .is_none()
237 {
238 return Err(OriginateError::VariablesNotSupported(endpoint.kind()));
239 }
240 }
241 endpoints.push(endpoint);
242 }
243 if !endpoints.is_empty() {
244 groups.push(endpoints);
245 }
246 }
247
248 let bridge = Self { variables, groups };
249 bridge.check_legs(block_parse)?;
250 Ok(bridge)
251 }
252
253 fn check_legs(&self, block_parse: BlockParse) -> Result<(), OriginateError> {
255 let target = DialStringTarget::new(CARRIER).with_block_parse(block_parse);
256 match self
257 .groups
258 .iter()
259 .position(|group| endpoints_interfere(group, target))
260 {
261 Some(group) => Err(OriginateError::BracketSpansLegs { group }),
262 None => Ok(()),
263 }
264 }
265}
266
267type LegReading = (Vec<Block>, String);
269
270fn reading(leg: &Leg) -> LegReading {
271 (
272 leg.blocks
273 .clone(),
274 leg.endpoint
275 .clone(),
276 )
277}
278
279fn read_group(text: &str, target: DialStringTarget) -> Option<Vec<LegReading>> {
281 let list = pipeline::read(text, target).ok()?;
282 let [thread] = &list.threads[..] else {
283 return None;
284 };
285 let [group] = &thread.groups[..] else {
286 return None;
287 };
288 Some(
289 group
290 .iter()
291 .map(reading)
292 .collect(),
293 )
294}
295
296fn legs_interfere<'a>(
299 texts: impl IntoIterator<Item = &'a str>,
300 joined: Option<Vec<LegReading>>,
301 target: DialStringTarget,
302) -> bool {
303 let Some(alone) = texts
304 .into_iter()
305 .map(|text| read_group(text, target))
306 .collect::<Option<Vec<_>>>()
307 else {
308 return false;
309 };
310 joined != Some(alone.concat())
311}
312
313fn endpoints_interfere(group: &[Endpoint], target: DialStringTarget) -> bool {
315 let rendered: Vec<String> = group
316 .iter()
317 .map(|endpoint| {
318 endpoint
319 .display_for(target)
320 .to_string()
321 })
322 .collect();
323 let joined = read_group(&rendered.join(","), target);
324 legs_interfere(
325 rendered
326 .iter()
327 .map(String::as_str),
328 joined,
329 target,
330 )
331}
332
333#[cfg(feature = "serde")]
334mod config {
335 use crate::commands::endpoint::Endpoint;
336 use crate::commands::variables::Variables;
337
338 #[derive(serde::Deserialize)]
339 pub(super) struct BridgeDialString {
340 #[serde(default)]
341 pub(super) variables: Option<Variables>,
342 pub(super) groups: Vec<Vec<Endpoint>>,
343 }
344}
345
346#[cfg(feature = "serde")]
347impl TryFrom<config::BridgeDialString> for BridgeDialString {
348 type Error = OriginateError;
349
350 fn try_from(config: config::BridgeDialString) -> Result<Self, Self::Error> {
351 let bridge = Self {
352 variables: config.variables,
353 groups: config.groups,
354 };
355 bridge.check_legs(BlockParse::default())?;
356 Ok(bridge)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::commands::endpoint::{
364 AudioEndpoint, DialString, ErrorEndpoint, LoopbackEndpoint, SofiaEndpoint, SofiaGateway,
365 };
366 use crate::commands::variables::{BlockParse, VariablesType};
367
368 #[cfg(feature = "serde")]
371 #[test]
372 fn a_bracket_spanning_legs_after_expansion_is_refused() {
373 let bridge = BridgeDialString::new(vec![vec![
374 crate::commands::endpoint::UserEndpoint::new("['").into(),
375 SofiaGateway::new("", "]")
376 .with_profile(" ")
377 .into(),
378 ]]);
379 let json = serde_json::to_value(&bridge).unwrap();
380 assert!(matches!(
381 serde_json::from_value::<BridgeDialString>(json)
382 .map_err(|e| e.to_string()),
383 Err(e) if e.contains("group 0")
384 ));
385 }
386
387 #[test]
391 fn display_and_from_str_are_the_default_revision() {
392 let mut vars = Variables::new(VariablesType::Default);
393 vars.insert("cid", "it's");
394 let mut ep_vars = Variables::new(VariablesType::Channel);
395 ep_vars.insert("path", r"C:\path");
396 let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
397 .with_variables(ep_vars)
398 .into()]])
399 .with_variables(vars);
400
401 let rendered = bridge
402 .display_with(BlockParse::PairSplitCleans)
403 .to_string();
404 assert_eq!(rendered, bridge.to_string());
405 assert_eq!(
406 BridgeDialString::parse_with(&rendered, BlockParse::PairSplitCleans)
407 .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}")),
408 bridge
409 );
410 }
411
412 #[test]
416 fn bridge_renders_one_level_shallower_than_the_default() {
417 let mut vars = Variables::new(VariablesType::Default);
418 vars.insert("cid", "it's");
419 let ep = SofiaGateway::new("gw", "18005551234");
420 let bridge = BridgeDialString::new(vec![vec![ep.into()]]).with_variables(vars.clone());
421
422 assert_eq!(
423 bridge.to_string(),
424 r"{cid=it\\\\\\'s}sofia/gateway/gw/18005551234"
425 );
426 assert_eq!(vars.to_string(), r"{cid=it\\\\\\\'s}");
427 }
428
429 #[test]
433 fn per_endpoint_escaped_values_round_trip() {
434 let mut ep_vars = Variables::new(VariablesType::Channel);
435 ep_vars.insert("path", r"C:\path");
436 ep_vars.insert("other", "a,b");
437 let bridge = BridgeDialString::new(vec![vec![SofiaGateway::new("gw", "1234")
438 .with_variables(ep_vars)
439 .into()]]);
440
441 let rendered = bridge.to_string();
442 let back: BridgeDialString = rendered
443 .parse()
444 .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
445 assert_eq!(back, bridge, "rendered {rendered}");
446 }
447
448 #[test]
452 fn per_endpoint_quoted_value_is_refused() {
453 let err = r"[cid=it\\\\\\'s]sofia/gateway/gw/1234"
454 .parse::<BridgeDialString>()
455 .unwrap_err()
456 .to_string();
457 assert!(
458 err.contains("cid") && err.contains("channel scope"),
459 "error does not name the variable and scope: {err}"
460 );
461 }
462
463 #[test]
464 fn display_single_endpoint() {
465 let bridge =
466 BridgeDialString::new(vec![vec![
467 SofiaGateway::new("my_provider", "18005551234").into()
468 ]]);
469 assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
470 }
471
472 #[test]
473 fn display_simultaneous_ring() {
474 let bridge = BridgeDialString::new(vec![vec![
475 SofiaGateway::new("primary", "18005551234").into(),
476 SofiaGateway::new("secondary", "18005551234").into(),
477 ]]);
478 assert_eq!(
479 bridge.to_string(),
480 "sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
481 );
482 }
483
484 #[test]
485 fn display_sequential_failover() {
486 let bridge = BridgeDialString::new(vec![
487 vec![SofiaGateway::new("primary", "18005551234").into()],
488 vec![SofiaGateway::new("backup", "18005551234").into()],
489 ]);
490 assert_eq!(
491 bridge.to_string(),
492 "sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
493 );
494 }
495
496 #[test]
497 fn display_mixed_simultaneous_and_sequential() {
498 let bridge = BridgeDialString::new(vec![
499 vec![
500 SofiaGateway::new("primary", "1234").into(),
501 SofiaGateway::new("secondary", "1234").into(),
502 ],
503 vec![SofiaGateway::new("backup", "1234").into()],
504 ]);
505 assert_eq!(
506 bridge.to_string(),
507 "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
508 );
509 }
510
511 #[test]
512 fn display_with_global_variables() {
513 let mut vars = Variables::new(VariablesType::Default);
514 vars.insert("hangup_after_bridge", "true");
515 let bridge =
516 BridgeDialString::new(vec![vec![
517 SofiaEndpoint::new("internal", "1000@domain").into()
518 ]])
519 .with_variables(vars);
520 assert_eq!(
521 bridge.to_string(),
522 "{hangup_after_bridge=true}sofia/internal/1000@domain"
523 );
524 }
525
526 #[test]
527 fn display_with_per_endpoint_variables() {
528 let mut ep_vars = Variables::new(VariablesType::Channel);
529 ep_vars.insert("leg_timeout", "30");
530 let bridge = BridgeDialString::new(vec![vec![
531 SofiaGateway::new("gw1", "1234")
532 .with_variables(ep_vars)
533 .into(),
534 SofiaGateway::new("gw2", "1234").into(),
535 ]]);
536 assert_eq!(
537 bridge.to_string(),
538 "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
539 );
540 }
541
542 #[test]
543 fn display_with_error_endpoint_failover() {
544 let bridge = BridgeDialString::new(vec![
545 vec![SofiaGateway::new("primary", "1234").into()],
546 vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
547 ]);
548 assert_eq!(
549 bridge.to_string(),
550 "sofia/gateway/primary/1234|error/USER_BUSY"
551 );
552 }
553
554 #[test]
555 fn display_with_loopback() {
556 let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
557 .with_context("default")
558 .into()]]);
559 assert_eq!(bridge.to_string(), "loopback/9199/default");
560 }
561
562 #[test]
565 fn from_str_single_endpoint() {
566 let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
567 .parse()
568 .unwrap();
569 assert_eq!(
570 bridge
571 .groups()
572 .len(),
573 1
574 );
575 assert_eq!(bridge.groups()[0].len(), 1);
576 assert!(bridge
577 .variables()
578 .is_none());
579 }
580
581 #[test]
582 fn from_str_simultaneous_ring() {
583 let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/1234"
584 .parse()
585 .unwrap();
586 assert_eq!(
587 bridge
588 .groups()
589 .len(),
590 1
591 );
592 assert_eq!(bridge.groups()[0].len(), 2);
593 }
594
595 #[test]
596 fn from_str_sequential_failover() {
597 let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
598 .parse()
599 .unwrap();
600 assert_eq!(
601 bridge
602 .groups()
603 .len(),
604 2
605 );
606 assert_eq!(bridge.groups()[0].len(), 1);
607 assert_eq!(bridge.groups()[1].len(), 1);
608 }
609
610 #[test]
611 fn from_str_mixed() {
612 let bridge: BridgeDialString =
613 "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
614 .parse()
615 .unwrap();
616 assert_eq!(
617 bridge
618 .groups()
619 .len(),
620 2
621 );
622 assert_eq!(bridge.groups()[0].len(), 2);
623 assert_eq!(bridge.groups()[1].len(), 1);
624 }
625
626 #[test]
627 fn from_str_with_global_variables() {
628 let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
629 .parse()
630 .unwrap();
631 assert!(bridge
632 .variables()
633 .is_some());
634 assert_eq!(
635 bridge
636 .variables()
637 .unwrap()
638 .get("hangup_after_bridge"),
639 Some("true")
640 );
641 assert_eq!(
642 bridge
643 .groups()
644 .len(),
645 1
646 );
647 assert_eq!(bridge.groups()[0].len(), 1);
648 }
649
650 #[test]
651 fn from_str_with_per_endpoint_variables() {
652 let bridge: BridgeDialString =
653 "[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
654 .parse()
655 .unwrap();
656 assert_eq!(
657 bridge
658 .groups()
659 .len(),
660 1
661 );
662 assert_eq!(bridge.groups()[0].len(), 2);
663 let ep = &bridge.groups()[0][0];
664 if let Endpoint::SofiaGateway(gw) = ep {
665 assert!(gw
666 .variables
667 .is_some());
668 } else {
669 panic!("expected SofiaGateway");
670 }
671 }
672
673 #[test]
676 fn from_str_with_enterprise_global_variables() {
677 let bridge: BridgeDialString = "<originate_timeout=60>sofia/internal/1000@domain"
678 .parse()
679 .unwrap();
680 assert_eq!(
681 bridge
682 .variables()
683 .expect("enterprise block is global")
684 .get("originate_timeout"),
685 Some("60")
686 );
687 assert_eq!(bridge.groups()[0].len(), 1);
688 }
689
690 #[test]
693 fn from_str_leaves_a_channel_block_on_its_endpoint() {
694 let bridge: BridgeDialString = "[leg_timeout=30]sofia/gateway/gw1/1234"
695 .parse()
696 .unwrap();
697 assert!(bridge
698 .variables()
699 .is_none());
700 assert!(bridge.groups()[0][0]
701 .variables()
702 .is_some());
703 }
704
705 #[test]
706 fn from_str_round_trip_single() {
707 let input = "sofia/gateway/my_provider/18005551234";
708 let bridge: BridgeDialString = input
709 .parse()
710 .unwrap();
711 assert_eq!(bridge.to_string(), input);
712 }
713
714 #[test]
715 fn from_str_round_trip_mixed() {
716 let input =
717 "sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
718 let bridge: BridgeDialString = input
719 .parse()
720 .unwrap();
721 assert_eq!(bridge.to_string(), input);
722 }
723
724 #[test]
725 fn from_str_round_trip_with_global_vars() {
726 let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
727 let bridge: BridgeDialString = input
728 .parse()
729 .unwrap();
730 assert_eq!(bridge.to_string(), input);
731 }
732
733 #[test]
736 fn serde_round_trip_single() {
737 let bridge =
738 BridgeDialString::new(vec![vec![
739 SofiaGateway::new("my_provider", "18005551234").into()
740 ]]);
741 let json = serde_json::to_string(&bridge).unwrap();
742 let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
743 assert_eq!(bridge, parsed);
744 }
745
746 #[test]
747 fn serde_round_trip_multi_group() {
748 let mut vars = Variables::new(VariablesType::Default);
749 vars.insert("hangup_after_bridge", "true");
750 let bridge = BridgeDialString::new(vec![
751 vec![
752 SofiaGateway::new("primary", "1234").into(),
753 SofiaGateway::new("secondary", "1234").into(),
754 ],
755 vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
756 ])
757 .with_variables(vars);
758 let json = serde_json::to_string(&bridge).unwrap();
759 let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
760 assert_eq!(bridge, parsed);
761 }
762
763 #[test]
766 fn from_str_empty_string_rejected() {
767 let result = "".parse::<BridgeDialString>();
768 assert!(result.is_err());
769 }
770
771 #[test]
772 fn from_str_whitespace_only_rejected() {
773 let result = " ".parse::<BridgeDialString>();
774 assert!(result.is_err());
775 }
776
777 #[test]
778 fn from_str_empty_groups_from_trailing_pipe() {
779 let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
781 .parse()
782 .unwrap();
783 assert_eq!(
784 bridge
785 .groups()
786 .len(),
787 1
788 );
789 }
790
791 #[test]
792 fn from_str_empty_variable_block() {
793 let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
794 .parse()
795 .unwrap();
796 assert!(bridge
797 .variables()
798 .is_none());
799 assert_eq!(
800 bridge
801 .groups()
802 .len(),
803 1
804 );
805 }
806
807 #[test]
808 fn from_str_mismatched_bracket_rejected() {
809 let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
810 assert!(result.is_err());
811 }
812
813 #[test]
815 fn a_separator_in_endpoint_text_round_trips() {
816 let bridge = BridgeDialString::new(vec![
817 vec![
818 SofiaEndpoint::new("internal", "a,b").into(),
819 SofiaEndpoint::new("internal", "c|d").into(),
820 ],
821 vec![LoopbackEndpoint::new("it's a").into()],
822 ]);
823 let rendered = bridge.to_string();
824 let back: BridgeDialString = rendered
825 .parse()
826 .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
827 assert_eq!(back, bridge, "rendered {rendered}");
828 }
829
830 #[test]
833 fn a_bracket_spanning_legs_is_refused_at_config_load() {
834 let spanning = r#"{"groups":[[{"sofia":{"profile":"internal","destination":"x["}},{"sofia":{"profile":"internal","destination":"y]"}}]]}"#;
835 assert!(serde_json::from_str::<BridgeDialString>(spanning).is_err());
836 for fine in [
837 r#"{"groups":[[{"sofia":{"profile":"internal","destination":"x[]"}},{"sofia":{"profile":"internal","destination":"y]"}}]]}"#,
838 r#"{"groups":[[{"sofia":{"profile":"internal","destination":"x["}}],[{"sofia":{"profile":"internal","destination":"y]"}}]]}"#,
839 ] {
840 assert!(
841 serde_json::from_str::<BridgeDialString>(fine).is_ok(),
842 "{fine}"
843 );
844 }
845 }
846
847 fn bracket_around_block(sep: char, value: &str) -> BridgeDialString {
849 let mut vars = Variables::new(VariablesType::Channel)
850 .with_separator(sep)
851 .unwrap();
852 vars.insert("v0", value);
853 BridgeDialString::new(vec![vec![
854 Endpoint::PortAudio(AudioEndpoint::new().with_destination("[\\")),
855 SofiaGateway::new("]", "")
856 .with_variables(vars)
857 .into(),
858 ]])
859 }
860
861 #[test]
864 fn a_comma_head_the_scan_rewrites_round_trips() {
865 let bridge = bracket_around_block(',', "undef");
866 let rendered = bridge.to_string();
867 let back: BridgeDialString = rendered
868 .parse()
869 .unwrap_or_else(|e| panic!("{rendered} failed to parse: {e}"));
870 assert_eq!(
871 back.to_string(),
872 r"portaudio/[\\\\\\\\,[v0=undef]sofia/gateway/]/"
873 );
874 let json = serde_json::to_value(&bridge).unwrap();
875 assert_eq!(
876 serde_json::from_value::<BridgeDialString>(json).unwrap(),
877 bridge
878 );
879 }
880
881 #[test]
884 fn a_range_rewriting_a_later_legs_value_is_refused() {
885 let bridge = bracket_around_block(':', "a,b");
886 let rendered = bridge.to_string();
887 assert!(
888 matches!(
889 rendered.parse::<BridgeDialString>(),
890 Err(OriginateError::BracketSpansLegs { group: 0 })
891 ),
892 "{rendered}: {:?}",
893 rendered.parse::<BridgeDialString>()
894 );
895 let json = serde_json::to_value(&bridge).unwrap();
896 assert!(serde_json::from_value::<BridgeDialString>(json).is_err());
897 }
898
899 #[test]
902 fn parse_reads_blocks_as_the_switch_installs_them() {
903 let bridge: BridgeDialString = r"{k=a\\\\b}[j=c\\\\\\\\d]loopback/9199/test"
904 .parse()
905 .unwrap_or_else(|e| panic!("{e}"));
906 assert_eq!(
907 bridge
908 .variables()
909 .and_then(|vars| vars.get("k")),
910 Some(r"a\b")
911 );
912 assert_eq!(
913 bridge.groups()[0][0]
914 .variables()
915 .and_then(|vars| vars.get("j")),
916 Some(r"c\d")
917 );
918 }
919
920 #[test]
921 fn serde_to_display_wire_format() {
922 let json = r#"{
923 "groups": [[{
924 "sofia_gateway": {
925 "gateway": "my_gw",
926 "destination": "18005551234"
927 }
928 }]]
929 }"#;
930 let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
931 assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
932 }
933}