freeswitch-types 1.6.0

FreeSWITCH ESL protocol types: channel state, events, headers, commands, and variables
Documentation
use std::str::FromStr;

use super::strip_endpoint_prefix;
use crate::commands::originate::{OriginateError, DEFAULT_CONTEXT};
use crate::commands::variables::DialStringCarrier;
use crate::commands::variables::Variables;

/// Internal loopback endpoint: `loopback/{extension}[/{context}[/{dialplan}]]`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct LoopbackEndpoint {
    /// Extension number or pattern.
    pub extension: String,
    /// Dialplan context. `None` omits the context segment, letting
    /// FreeSWITCH use its default.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub context: Option<String>,
    /// Dialplan for the re-entered leg. `None` omits the segment, letting
    /// FreeSWITCH use its default.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub dialplan: Option<String>,
    /// Per-channel variables prepended as `{key=value}`.
    #[cfg_attr(
        feature = "serde",
        serde(default, skip_serializing_if = "Option::is_none")
    )]
    pub variables: Option<Variables>,
}

impl LoopbackEndpoint {
    /// Create a new loopback endpoint with no explicit context.
    pub fn new(extension: impl Into<String>) -> Self {
        Self {
            extension: extension.into(),
            context: None,
            dialplan: None,
            variables: None,
        }
    }

    /// Set the dialplan context.
    pub fn with_context(mut self, context: impl Into<String>) -> Self {
        self.context = Some(context.into());
        self
    }

    /// Set the dialplan. It is the third positional segment, so rendering one
    /// without a context emits [`DEFAULT_CONTEXT`] in the gap.
    pub fn with_dialplan(mut self, dialplan: impl Into<String>) -> Self {
        self.dialplan = Some(dialplan.into());
        self
    }
}

impl_dial_string_with_variables!(
    LoopbackEndpoint,
    |this, f| match (&this.context, &this.dialplan) {
        (None, None) => write!(f, "loopback/{}", this.extension),
        (Some(ctx), None) => write!(f, "loopback/{}/{}", this.extension, ctx),
        (ctx, Some(dialplan)) => write!(
            f,
            "loopback/{}/{}/{}",
            this.extension,
            ctx.as_deref()
                .unwrap_or(DEFAULT_CONTEXT),
            dialplan
        ),
    }
);

impl FromStr for LoopbackEndpoint {
    type Err = OriginateError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (variables, rest) =
            strip_endpoint_prefix(s, "loopback/", "loopback", DialStringCarrier::EslApi)?;
        let mut segments = rest.splitn(3, '/');
        let extension = segments
            .next()
            .unwrap_or(rest);
        Ok(Self {
            extension: extension.into(),
            context: segments
                .next()
                .map(str::to_string),
            dialplan: segments
                .next()
                .map(str::to_string),
            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);
    }

    /// `mod_loopback` splits the dial string into extension, context and
    /// dialplan. Modelling two of the three folds `default/xml` into the
    /// context, which then renders back as one segment the switch re-splits.
    #[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");
    }

    /// The dialplan is the third positional segment, so it cannot be written
    /// without the second; the builder supplies the switch's own default.
    #[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);
    }
}