use super::{after_prefix, check_field, undeliverable, EndpointFieldFault};
use crate::commands::originate::{OriginateError, DEFAULT_CONTEXT};
use crate::commands::variables::Variables;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "config::LoopbackEndpoint"))]
#[non_exhaustive]
pub struct LoopbackEndpoint {
pub extension: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub context: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub dialplan: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub variables: Option<Variables>,
}
impl LoopbackEndpoint {
pub fn new(extension: impl Into<String>) -> Self {
Self {
extension: extension.into(),
context: None,
dialplan: None,
variables: None,
}
}
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
pub fn with_dialplan(mut self, dialplan: impl Into<String>) -> Self {
self.dialplan = Some(dialplan.into());
self
}
}
impl_dial_string_with_variables!(LoopbackEndpoint, write_module_text, |this| match (
&this.context,
&this.dialplan
) {
(None, None) => format!("loopback/{}", this.extension),
(Some(ctx), None) => format!("loopback/{}/{}", this.extension, ctx),
(ctx, Some(dialplan)) => format!(
"loopback/{}/{}/{}",
this.extension,
ctx.as_deref()
.unwrap_or(DEFAULT_CONTEXT),
dialplan
),
});
const KIND: &str = "loopback";
fn is_application(extension: &str) -> bool {
extension
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("app="))
}
impl LoopbackEndpoint {
pub(crate) fn check_deliverable(&self) -> Result<(), OriginateError> {
if is_application(&self.extension) {
let name = self
.extension
.split_once(':')
.map_or(
self.extension
.as_str(),
|(name, _)| name,
);
check_field(KIND, "extension", name, &["/"])?;
check_field(KIND, "extension", &self.extension, &[])?;
for (field, value) in [("context", &self.context), ("dialplan", &self.dialplan)] {
if value.is_some() {
return Err(undeliverable(
KIND,
field,
EndpointFieldFault::FollowsAnApplication,
));
}
}
return Ok(());
}
check_field(KIND, "extension", &self.extension, &["/"])?;
for (field, value, separators) in [
("context", &self.context, &["/"][..]),
("dialplan", &self.dialplan, &[][..]),
] {
let Some(value) = value else {
continue;
};
check_field(KIND, field, value, separators)?;
if value.is_empty() {
return Err(undeliverable(
KIND,
field,
EndpointFieldFault::EmptyReadsAsDefault,
));
}
}
Ok(())
}
pub(crate) fn parse_bare(text: &str) -> Result<Self, OriginateError> {
let rest = after_prefix(text, "loopback/", KIND)?;
let ep = if is_application(rest) {
Self::new(rest)
} else {
let mut segments = rest.splitn(3, '/');
let mut ep = Self::new(
segments
.next()
.unwrap_or(rest),
);
ep.context = segments
.next()
.map(str::to_string);
ep.dialplan = segments
.next()
.map(str::to_string);
ep
};
ep.check_deliverable()?;
Ok(ep)
}
}
impl_endpoint_parse!(from_str: LoopbackEndpoint);
impl_endpoint_parse!(config:
LoopbackEndpoint {
extension: String,
;
context: Option<String>,
dialplan: Option<String>,
variables: Option<Variables>,
}
);
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::variables::VariablesType;
#[test]
fn loopback_display_no_context() {
let ep = LoopbackEndpoint::new("9199");
assert_eq!(ep.to_string(), "loopback/9199");
}
#[test]
fn loopback_display_with_context() {
let ep = LoopbackEndpoint::new("9199").with_context("default");
assert_eq!(ep.to_string(), "loopback/9199/default");
}
#[test]
fn loopback_display_with_variables() {
let mut vars = Variables::new(VariablesType::Default);
vars.insert("loopback_initial_codec", "L16@48000h");
let ep = LoopbackEndpoint::new("100")
.with_context("test")
.with_variables(vars);
assert_eq!(
ep.to_string(),
"{loopback_initial_codec=L16@48000h}loopback/100/test"
);
}
#[test]
fn loopback_from_str_with_context() {
let ep: LoopbackEndpoint = "loopback/9199/test"
.parse()
.unwrap();
assert_eq!(ep.extension, "9199");
assert_eq!(
ep.context
.as_deref(),
Some("test")
);
}
#[test]
fn loopback_from_str_no_context() {
let ep: LoopbackEndpoint = "loopback/9199"
.parse()
.unwrap();
assert_eq!(ep.extension, "9199");
assert!(ep
.context
.is_none());
}
#[test]
fn loopback_round_trip_with_context() {
let ep = LoopbackEndpoint::new("100").with_context("myctx");
let s = ep.to_string();
let parsed: LoopbackEndpoint = s
.parse()
.unwrap();
assert_eq!(parsed, ep);
}
#[test]
fn loopback_carries_a_dialplan_segment() {
let ep: LoopbackEndpoint = "loopback/9199/default/xml"
.parse()
.unwrap();
assert_eq!(ep.extension, "9199");
assert_eq!(
ep.context
.as_deref(),
Some("default")
);
assert_eq!(
ep.dialplan
.as_deref(),
Some("xml")
);
assert_eq!(ep.to_string(), "loopback/9199/default/xml");
}
#[test]
fn loopback_dialplan_without_a_context_fills_the_gap() {
let ep = LoopbackEndpoint::new("9199").with_dialplan("inline");
assert_eq!(ep.to_string(), "loopback/9199/default/inline");
assert_eq!(
ep.to_string()
.parse::<LoopbackEndpoint>()
.unwrap()
.dialplan
.as_deref(),
Some("inline")
);
}
#[test]
fn loopback_round_trip_no_context() {
let ep = LoopbackEndpoint::new("9199");
let s = ep.to_string();
let parsed: LoopbackEndpoint = s
.parse()
.unwrap();
assert_eq!(parsed, ep);
}
#[test]
fn loopback_display_parse_display_stable() {
let inputs = [
"loopback/9199",
"loopback/100/default",
"loopback/ext123/custom_ctx",
"loopback/9199/default/xml",
"loopback/100/custom_ctx/inline",
];
for input in inputs {
let parsed: LoopbackEndpoint = input
.parse()
.unwrap();
let displayed = parsed.to_string();
assert_eq!(displayed, input, "round-trip failed for: {}", input);
let reparsed: LoopbackEndpoint = displayed
.parse()
.unwrap();
assert_eq!(reparsed, parsed);
}
}
#[test]
fn serde_loopback_endpoint() {
let ep = LoopbackEndpoint::new("9199").with_context("default");
let json = serde_json::to_string(&ep).unwrap();
let parsed: LoopbackEndpoint = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ep);
}
}