use std::io::{self, Write};
use std::str::FromStr;
use freeswitch_log_parser::MessageKind;
use freeswitch_types::commands::endpoint::DialString;
use freeswitch_types::{BridgeDialString, EslArray};
pub fn dial_string_of(kind: &MessageKind) -> Option<&str> {
match kind {
MessageKind::Execute {
application,
arguments,
..
} if matches!(application.as_str(), "bridge" | "att_xfer") => Some(arguments),
_ => None,
}
}
pub fn print_dial_string(
w: &mut dyn Write,
arguments: &str,
label_color: &str,
value_color: &str,
reset: &str,
) -> io::Result<()> {
let Ok(dial) = BridgeDialString::from_str(arguments) else {
return Ok(());
};
if let Some(vars) = dial.variables() {
for (key, value) in vars.iter() {
print_variable(w, key, value, label_color, value_color, reset)?;
}
}
let groups = dial.groups();
let total: usize = groups.iter().map(|g| g.len()).sum();
for (i, group) in groups.iter().enumerate() {
let simultaneous = |n: usize| match n {
1 => String::new(),
n => format!(" ({n} simultaneous)"),
};
let header = if groups.len() > 1 {
format!(
"failover {} of {}{}",
i + 1,
groups.len(),
simultaneous(group.len())
)
} else if total > 1 {
format!("endpoints{}", simultaneous(total))
} else {
"endpoint".to_string()
};
writeln!(w, "{label_color} dial {header}{reset}")?;
for ep in group {
let ext = ep
.variables()
.and_then(|v| v.get("presence_id"))
.map(|id| format!("ext={} ", id.split('@').next().unwrap_or(id)))
.unwrap_or_default();
writeln!(w, "{value_color} dial {ext}{ep}{reset}")?;
}
}
Ok(())
}
fn print_variable(
w: &mut dyn Write,
key: &str,
value: &str,
label_color: &str,
value_color: &str,
reset: &str,
) -> io::Result<()> {
match EslArray::parse(value) {
Ok(arr) if arr.len() > 1 => {
writeln!(
w,
"{label_color} dial {key}: ({} entries){reset}",
arr.len()
)?;
for item in arr.items() {
writeln!(w, "{value_color} dial {item}{reset}")?;
}
}
_ => writeln!(
w,
"{label_color} dial {key}: {value_color}{value}{reset}"
)?,
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn render(arguments: &str) -> String {
let mut out: Vec<u8> = Vec::new();
print_dial_string(&mut out, arguments, "", "", "").unwrap();
String::from_utf8(out).unwrap()
}
fn execute(application: &str, arguments: &str) -> MessageKind {
MessageKind::Execute {
depth: 0,
channel: "sofia/internal/1001".to_string(),
application: application.to_string(),
arguments: arguments.to_string(),
}
}
#[test]
fn only_dial_string_applications_expand() {
assert!(dial_string_of(&execute("bridge", "sofia/gw/1")).is_some());
assert!(dial_string_of(&execute("att_xfer", "sofia/gw/1")).is_some());
assert!(dial_string_of(&execute("originate", "sofia/gw/1")).is_none());
assert!(dial_string_of(&execute("set", "a=b")).is_none());
assert!(dial_string_of(&MessageKind::General).is_none());
}
#[test]
fn global_variables_and_single_endpoint() {
let out = render("{ignore_early_media=true}sofia/gateway/gw/5551234");
assert!(out.contains("dial ignore_early_media: true"), "{out}");
assert!(out.contains("dial endpoint\n"), "{out}");
assert!(out.contains("sofia/gateway/gw/5551234"), "{out}");
}
#[test]
fn simultaneous_endpoints_counted() {
let out =
render("sofia/internal/1001@pbx.example.test,sofia/internal/1002@pbx.example.test");
assert!(out.contains("dial endpoints (2 simultaneous)"), "{out}");
assert!(
out.contains("sofia/internal/1001@pbx.example.test"),
"{out}"
);
assert!(
out.contains("sofia/internal/1002@pbx.example.test"),
"{out}"
);
}
#[test]
fn failover_groups_are_numbered() {
let out = render("sofia/gateway/primary/5551234|sofia/gateway/backup/5551234");
assert!(out.contains("dial failover 1 of 2\n"), "{out}");
assert!(out.contains("dial failover 2 of 2\n"), "{out}");
}
#[test]
fn simultaneous_endpoints_inside_a_failover_group() {
let out = render("sofia/gw/a/1,sofia/gw/b/1|sofia/gw/c/1");
assert!(
out.contains("dial failover 1 of 2 (2 simultaneous)"),
"{out}"
);
assert!(out.contains("dial failover 2 of 2\n"), "{out}");
}
#[test]
fn presence_id_becomes_an_extension_hint() {
let out = render("[presence_id=1001@pbx.example.test]sofia/internal/1001@pbx.example.test");
assert!(out.contains("ext=1001 "), "{out}");
}
#[test]
fn array_values_expand_to_one_line_each() {
let out = render(
"{sip_h_X-Tag=ARRAY::<urn:example:one>;purpose=first\
|:<urn:example:two>;purpose=second}sofia/gateway/gw/5551234",
);
assert!(out.contains("sip_h_X-Tag: (2 entries)"), "{out}");
assert!(out.contains("<urn:example:one>;purpose=first"), "{out}");
assert!(out.contains("<urn:example:two>;purpose=second"), "{out}");
}
#[test]
fn unparseable_arguments_render_nothing() {
assert_eq!(render(""), "");
}
}