use std::fmt;
use std::str::FromStr;
use super::endpoint::Endpoint;
use super::find_matching_bracket;
use super::originate::{OriginateError, Variables};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BridgeDialString {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
variables: Option<Variables>,
groups: Vec<Vec<Endpoint>>,
}
impl BridgeDialString {
pub fn new(groups: Vec<Vec<Endpoint>>) -> Self {
Self {
variables: None,
groups,
}
}
pub fn with_variables(mut self, variables: Variables) -> Self {
self.variables = Some(variables);
self
}
pub fn variables(&self) -> Option<&Variables> {
self.variables
.as_ref()
}
pub fn groups(&self) -> &[Vec<Endpoint>] {
&self.groups
}
pub fn variables_mut(&mut self) -> &mut Option<Variables> {
&mut self.variables
}
pub fn groups_mut(&mut self) -> &mut Vec<Vec<Endpoint>> {
&mut self.groups
}
}
impl fmt::Display for BridgeDialString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(vars) = &self.variables {
if !vars.is_empty() {
write!(f, "{}", vars)?;
}
}
for (gi, group) in self
.groups
.iter()
.enumerate()
{
if gi > 0 {
f.write_str("|")?;
}
for (ei, ep) in group
.iter()
.enumerate()
{
if ei > 0 {
f.write_str(",")?;
}
write!(f, "{}", ep)?;
}
}
Ok(())
}
}
impl FromStr for BridgeDialString {
type Err = OriginateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(OriginateError::ParseError(
"empty bridge dial string".into(),
));
}
let (variables, rest) = if s.starts_with('{') {
let close = find_matching_bracket(s, '{', '}').ok_or_else(|| {
OriginateError::ParseError("unclosed { in bridge dial string".into())
})?;
let var_str = &s[..=close];
let vars: Variables = var_str.parse()?;
let vars = if vars.is_empty() { None } else { Some(vars) };
(vars, &s[close + 1..])
} else {
(None, s)
};
let group_strs = split_respecting_brackets(rest, '|');
let mut groups = Vec::new();
for group_str in &group_strs {
let group_str = group_str.trim();
if group_str.is_empty() {
continue;
}
let ep_strs = split_respecting_brackets(group_str, ',');
let mut endpoints = Vec::new();
for ep_str in &ep_strs {
let ep_str = ep_str.trim();
if ep_str.is_empty() {
continue;
}
let ep: Endpoint = ep_str.parse()?;
endpoints.push(ep);
}
if !endpoints.is_empty() {
groups.push(endpoints);
}
}
Ok(Self { variables, groups })
}
}
fn split_respecting_brackets(s: &str, sep: char) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut start = 0;
let bytes = s.as_bytes();
for (i, &b) in bytes
.iter()
.enumerate()
{
match b {
b'{' | b'[' | b'<' | b'(' => depth += 1,
b'}' | b']' | b'>' | b')' => {
depth -= 1;
if depth < 0 {
depth = 0;
}
}
_ if b == sep as u8 && depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
parts.push(&s[start..]);
parts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::endpoint::{ErrorEndpoint, LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
use crate::commands::originate::VariablesType;
#[test]
fn display_single_endpoint() {
let bridge =
BridgeDialString::new(vec![vec![
SofiaGateway::new("my_provider", "18005551234").into()
]]);
assert_eq!(bridge.to_string(), "sofia/gateway/my_provider/18005551234");
}
#[test]
fn display_simultaneous_ring() {
let bridge = BridgeDialString::new(vec![vec![
SofiaGateway::new("primary", "18005551234").into(),
SofiaGateway::new("secondary", "18005551234").into(),
]]);
assert_eq!(
bridge.to_string(),
"sofia/gateway/primary/18005551234,sofia/gateway/secondary/18005551234"
);
}
#[test]
fn display_sequential_failover() {
let bridge = BridgeDialString::new(vec![
vec![SofiaGateway::new("primary", "18005551234").into()],
vec![SofiaGateway::new("backup", "18005551234").into()],
]);
assert_eq!(
bridge.to_string(),
"sofia/gateway/primary/18005551234|sofia/gateway/backup/18005551234"
);
}
#[test]
fn display_mixed_simultaneous_and_sequential() {
let bridge = BridgeDialString::new(vec![
vec![
SofiaGateway::new("primary", "1234").into(),
SofiaGateway::new("secondary", "1234").into(),
],
vec![SofiaGateway::new("backup", "1234").into()],
]);
assert_eq!(
bridge.to_string(),
"sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
);
}
#[test]
fn display_with_global_variables() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("hangup_after_bridge", "true");
let bridge =
BridgeDialString::new(vec![vec![
SofiaEndpoint::new("internal", "1000@domain").into()
]])
.with_variables(vars);
assert_eq!(
bridge.to_string(),
"{hangup_after_bridge=true}sofia/internal/1000@domain"
);
}
#[test]
fn display_with_per_endpoint_variables() {
let mut ep_vars = Variables::new(VariablesType::Channel);
ep_vars.insert("leg_timeout", "30");
let bridge = BridgeDialString::new(vec![vec![
SofiaGateway::new("gw1", "1234")
.with_variables(ep_vars)
.into(),
SofiaGateway::new("gw2", "1234").into(),
]]);
assert_eq!(
bridge.to_string(),
"[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
);
}
#[test]
fn display_with_error_endpoint_failover() {
let bridge = BridgeDialString::new(vec![
vec![SofiaGateway::new("primary", "1234").into()],
vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
]);
assert_eq!(
bridge.to_string(),
"sofia/gateway/primary/1234|error/USER_BUSY"
);
}
#[test]
fn display_with_loopback() {
let bridge = BridgeDialString::new(vec![vec![LoopbackEndpoint::new("9199")
.with_context("default")
.into()]]);
assert_eq!(bridge.to_string(), "loopback/9199/default");
}
#[test]
fn from_str_single_endpoint() {
let bridge: BridgeDialString = "sofia/gateway/my_provider/18005551234"
.parse()
.unwrap();
assert_eq!(
bridge
.groups()
.len(),
1
);
assert_eq!(bridge.groups()[0].len(), 1);
assert!(bridge
.variables()
.is_none());
}
#[test]
fn from_str_simultaneous_ring() {
let bridge: BridgeDialString = "sofia/gateway/primary/1234,sofia/gateway/secondary/1234"
.parse()
.unwrap();
assert_eq!(
bridge
.groups()
.len(),
1
);
assert_eq!(bridge.groups()[0].len(), 2);
}
#[test]
fn from_str_sequential_failover() {
let bridge: BridgeDialString = "sofia/gateway/primary/1234|sofia/gateway/backup/1234"
.parse()
.unwrap();
assert_eq!(
bridge
.groups()
.len(),
2
);
assert_eq!(bridge.groups()[0].len(), 1);
assert_eq!(bridge.groups()[1].len(), 1);
}
#[test]
fn from_str_mixed() {
let bridge: BridgeDialString =
"sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234"
.parse()
.unwrap();
assert_eq!(
bridge
.groups()
.len(),
2
);
assert_eq!(bridge.groups()[0].len(), 2);
assert_eq!(bridge.groups()[1].len(), 1);
}
#[test]
fn from_str_with_global_variables() {
let bridge: BridgeDialString = "{hangup_after_bridge=true}sofia/internal/1000@domain"
.parse()
.unwrap();
assert!(bridge
.variables()
.is_some());
assert_eq!(
bridge
.variables()
.unwrap()
.get("hangup_after_bridge"),
Some("true")
);
assert_eq!(
bridge
.groups()
.len(),
1
);
assert_eq!(bridge.groups()[0].len(), 1);
}
#[test]
fn from_str_with_per_endpoint_variables() {
let bridge: BridgeDialString =
"[leg_timeout=30]sofia/gateway/gw1/1234,sofia/gateway/gw2/1234"
.parse()
.unwrap();
assert_eq!(
bridge
.groups()
.len(),
1
);
assert_eq!(bridge.groups()[0].len(), 2);
let ep = &bridge.groups()[0][0];
if let Endpoint::SofiaGateway(gw) = ep {
assert!(gw
.variables
.is_some());
} else {
panic!("expected SofiaGateway");
}
}
#[test]
fn from_str_round_trip_single() {
let input = "sofia/gateway/my_provider/18005551234";
let bridge: BridgeDialString = input
.parse()
.unwrap();
assert_eq!(bridge.to_string(), input);
}
#[test]
fn from_str_round_trip_mixed() {
let input =
"sofia/gateway/primary/1234,sofia/gateway/secondary/1234|sofia/gateway/backup/1234";
let bridge: BridgeDialString = input
.parse()
.unwrap();
assert_eq!(bridge.to_string(), input);
}
#[test]
fn from_str_round_trip_with_global_vars() {
let input = "{hangup_after_bridge=true}sofia/internal/1000@domain";
let bridge: BridgeDialString = input
.parse()
.unwrap();
assert_eq!(bridge.to_string(), input);
}
#[test]
fn serde_round_trip_single() {
let bridge =
BridgeDialString::new(vec![vec![
SofiaGateway::new("my_provider", "18005551234").into()
]]);
let json = serde_json::to_string(&bridge).unwrap();
let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
assert_eq!(bridge, parsed);
}
#[test]
fn serde_round_trip_multi_group() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("hangup_after_bridge", "true");
let bridge = BridgeDialString::new(vec![
vec![
SofiaGateway::new("primary", "1234").into(),
SofiaGateway::new("secondary", "1234").into(),
],
vec![ErrorEndpoint::new(crate::channel::HangupCause::UserBusy).into()],
])
.with_variables(vars);
let json = serde_json::to_string(&bridge).unwrap();
let parsed: BridgeDialString = serde_json::from_str(&json).unwrap();
assert_eq!(bridge, parsed);
}
#[test]
fn from_str_empty_string_rejected() {
let result = "".parse::<BridgeDialString>();
assert!(result.is_err());
}
#[test]
fn from_str_whitespace_only_rejected() {
let result = " ".parse::<BridgeDialString>();
assert!(result.is_err());
}
#[test]
fn from_str_empty_groups_from_trailing_pipe() {
let bridge: BridgeDialString = "sofia/gateway/gw1/1234|"
.parse()
.unwrap();
assert_eq!(
bridge
.groups()
.len(),
1
);
}
#[test]
fn from_str_empty_variable_block() {
let bridge: BridgeDialString = "{}sofia/gateway/gw1/1234"
.parse()
.unwrap();
assert!(bridge
.variables()
.is_none());
assert_eq!(
bridge
.groups()
.len(),
1
);
}
#[test]
fn from_str_mismatched_bracket_rejected() {
let result = "{unclosed=true sofia/gateway/gw1/1234".parse::<BridgeDialString>();
assert!(result.is_err());
}
#[test]
fn serde_to_display_wire_format() {
let json = r#"{
"groups": [[{
"sofia_gateway": {
"gateway": "my_gw",
"destination": "18005551234"
}
}]]
}"#;
let bridge: BridgeDialString = serde_json::from_str(json).unwrap();
assert_eq!(bridge.to_string(), "sofia/gateway/my_gw/18005551234");
}
}