freeswitch-types 1.0.0-rc.1

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

const ARRAY_HEADER: &str = "ARRAY::";
const ARRAY_SEPARATOR: &str = "|:";

/// Maximum items accepted by [`EslArray::parse`].
///
/// Matches the index bound in FreeSWITCH `switch_event.c`
/// (`switch_event_base_add_header`, `if (index > -1 && index <= 4000)`).
pub const MAX_ARRAY_ITEMS: usize = 4000;

/// Errors from [`EslArray::parse`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EslArrayError {
    /// Input did not start with the `ARRAY::` prefix.
    MissingPrefix,
    /// Item count exceeds [`MAX_ARRAY_ITEMS`].
    TooManyItems {
        /// Actual number of items found.
        count: usize,
        /// Configured maximum.
        max: usize,
    },
}

impl fmt::Display for EslArrayError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingPrefix => f.write_str("missing ARRAY:: prefix"),
            Self::TooManyItems { count, max } => {
                write!(f, "array has {count} items, maximum is {max}")
            }
        }
    }
}

impl std::error::Error for EslArrayError {}

/// Parses FreeSWITCH `ARRAY::item1|:item2|:item3` format
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EslArray(Vec<String>);

impl EslArray {
    /// Parse an `ARRAY::` formatted string.
    ///
    /// Returns [`EslArrayError::MissingPrefix`] if the prefix is absent, or
    /// [`EslArrayError::TooManyItems`] if the item count exceeds
    /// [`MAX_ARRAY_ITEMS`].
    pub fn parse(s: &str) -> Result<Self, EslArrayError> {
        let body = s
            .strip_prefix(ARRAY_HEADER)
            .ok_or(EslArrayError::MissingPrefix)?;
        let items: Vec<String> = body
            .split(ARRAY_SEPARATOR)
            .map(String::from)
            .collect();
        let count = items.len();
        if count > MAX_ARRAY_ITEMS {
            return Err(EslArrayError::TooManyItems {
                count,
                max: MAX_ARRAY_ITEMS,
            });
        }
        Ok(Self(items))
    }

    /// Create a new array from a vec of items.
    pub fn new(items: Vec<String>) -> Self {
        Self(items)
    }

    /// Append an item to the end.
    pub fn push(&mut self, value: String) {
        self.0
            .push(value);
    }

    /// Prepend an item to the front.
    pub fn unshift(&mut self, value: String) {
        self.0
            .insert(0, value);
    }

    /// The parsed array items.
    pub fn items(&self) -> &[String] {
        &self.0
    }

    /// Number of items in the array.
    pub fn len(&self) -> usize {
        self.0
            .len()
    }

    /// Returns `true` if the array has no items.
    pub fn is_empty(&self) -> bool {
        self.0
            .is_empty()
    }
}

impl<'a> IntoIterator for &'a EslArray {
    type Item = &'a String;
    type IntoIter = std::slice::Iter<'a, String>;

    fn into_iter(self) -> Self::IntoIter {
        self.0
            .iter()
    }
}

impl IntoIterator for EslArray {
    type Item = String;
    type IntoIter = std::vec::IntoIter<String>;

    fn into_iter(self) -> Self::IntoIter {
        self.0
            .into_iter()
    }
}

impl fmt::Display for EslArray {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(ARRAY_HEADER)?;
        for (i, item) in self
            .0
            .iter()
            .enumerate()
        {
            if i > 0 {
                f.write_str(ARRAY_SEPARATOR)?;
            }
            f.write_str(item)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_single_item() {
        let arr = EslArray::parse("ARRAY::hello").unwrap();
        assert_eq!(arr.items(), &["hello"]);
        assert_eq!(arr.len(), 1);
    }

    #[test]
    fn parse_multiple_items() {
        let arr = EslArray::parse("ARRAY::one|:two|:three").unwrap();
        assert_eq!(arr.items(), &["one", "two", "three"]);
        assert_eq!(arr.len(), 3);
    }

    #[test]
    fn parse_non_array_returns_missing_prefix() {
        assert!(matches!(
            EslArray::parse("not an array"),
            Err(EslArrayError::MissingPrefix)
        ));
        assert!(matches!(
            EslArray::parse(""),
            Err(EslArrayError::MissingPrefix)
        ));
        assert!(matches!(
            EslArray::parse("ARRAY:"),
            Err(EslArrayError::MissingPrefix)
        ));
    }

    #[test]
    fn parse_at_max_items_succeeds() {
        let items: Vec<&str> = (0..MAX_ARRAY_ITEMS)
            .map(|_| "x")
            .collect();
        let input = format!("ARRAY::{}", items.join("|:"));
        let arr = EslArray::parse(&input).unwrap();
        assert_eq!(arr.len(), MAX_ARRAY_ITEMS);
    }

    #[test]
    fn parse_over_max_items_returns_error() {
        let items: Vec<&str> = (0..=MAX_ARRAY_ITEMS)
            .map(|_| "x")
            .collect();
        let input = format!("ARRAY::{}", items.join("|:"));
        assert_eq!(
            EslArray::parse(&input),
            Err(EslArrayError::TooManyItems {
                count: MAX_ARRAY_ITEMS + 1,
                max: MAX_ARRAY_ITEMS,
            })
        );
    }

    #[test]
    fn error_display_missing_prefix() {
        assert_eq!(
            EslArrayError::MissingPrefix.to_string(),
            "missing ARRAY:: prefix"
        );
    }

    #[test]
    fn error_display_too_many_items() {
        let err = EslArrayError::TooManyItems {
            count: 5000,
            max: 4000,
        };
        assert_eq!(err.to_string(), "array has 5000 items, maximum is 4000");
    }

    #[test]
    fn display_round_trip() {
        let input = "ARRAY::one|:two|:three";
        let arr = EslArray::parse(input).unwrap();
        assert_eq!(arr.to_string(), input);
    }

    #[test]
    fn display_single_item() {
        let arr = EslArray::parse("ARRAY::only").unwrap();
        assert_eq!(arr.to_string(), "ARRAY::only");
    }

    #[test]
    fn empty_items_in_array() {
        let arr = EslArray::parse("ARRAY::|:|:stuff").unwrap();
        assert_eq!(arr.items(), &["", "", "stuff"]);
    }

    #[test]
    fn test_new() {
        let arr = EslArray::new(vec!["a".into(), "b".into(), "c".into()]);
        assert_eq!(arr.items(), &["a", "b", "c"]);
        assert_eq!(arr.len(), 3);
    }

    #[test]
    fn test_push() {
        let mut arr = EslArray::new(vec!["first".into()]);
        arr.push("second".into());
        arr.push("third".into());
        assert_eq!(arr.items(), &["first", "second", "third"]);
        assert_eq!(arr.to_string(), "ARRAY::first|:second|:third");
    }

    #[test]
    fn test_unshift() {
        let mut arr = EslArray::new(vec!["last".into()]);
        arr.unshift("middle".into());
        arr.unshift("first".into());
        assert_eq!(arr.items(), &["first", "middle", "last"]);
        assert_eq!(arr.to_string(), "ARRAY::first|:middle|:last");
    }

    #[test]
    fn parse_sip_uris_with_colons() {
        let input = "ARRAY::sip:alice@atlanta.example.com|:sip:bob@biloxi.example.com";
        let arr = EslArray::parse(input).unwrap();
        assert_eq!(
            arr.items(),
            &[
                "sip:alice@atlanta.example.com",
                "sip:bob@biloxi.example.com"
            ]
        );
        assert_eq!(arr.to_string(), input);
    }

    #[test]
    fn parse_sip_uris_with_angle_brackets_and_params() {
        let input = "ARRAY::<sip:+15551234567@gw.example.com;user=phone>|:<tel:+15559876543>";
        let arr = EslArray::parse(input).unwrap();
        assert_eq!(arr.len(), 2);
        assert_eq!(
            arr.items()[0],
            "<sip:+15551234567@gw.example.com;user=phone>"
        );
        assert_eq!(arr.items()[1], "<tel:+15559876543>");
        assert_eq!(arr.to_string(), input);
    }
}