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, MAX_PEERS};
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 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 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
307type LegReading = (Vec<Block>, String);
309
310fn reading(leg: &Leg) -> LegReading {
311 (
312 leg.blocks
313 .clone(),
314 leg.endpoint
315 .clone(),
316 )
317}
318
319fn 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
336fn 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
353fn 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 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 #[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 #[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 #[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 #[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}