ical_rust 0.8.6

A lightweight, efficient, and robust iCalendar (ics) parser and recurrence expander library
Documentation
use crate::{errors::BlockParseError, ical_line_parser::ICalLineParser};
use std::str::FromStr;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Block {
    pub name: String,
    pub inner_lines: Vec<String>,
    pub inner_blocks: Vec<Block>,
}

impl Block {
    pub fn name(&self) -> &str {
        self.name.as_ref()
    }

    pub fn from_lines<I, S>(lines: I) -> Result<Self, BlockParseError>
    where
        I: IntoIterator<Item = S>,
        S: Into<String> + AsRef<str>,
    {
        let mut stack: Vec<Block> = Vec::new();
        let mut root: Option<Block> = None;

        for line in lines {
            let s = line.as_ref();

            if root.is_some() {
                if s.trim().is_empty() {
                    continue;
                } else {
                    return Err(BlockParseError::UnexpectedContentAfterEnd);
                }
            }

            if s.len() >= 6 && s.is_char_boundary(6) && s[..6].eq_ignore_ascii_case("BEGIN:") {
                stack.push(Block {
                    name: s[6..].to_ascii_uppercase(),
                    inner_lines: Vec::new(),
                    inner_blocks: Vec::new(),
                });
            } else if s.len() >= 4 && s.is_char_boundary(4) && s[..4].eq_ignore_ascii_case("END:") {
                let end_name = s[4..].to_ascii_uppercase();
                let top = stack
                    .pop()
                    .ok_or(BlockParseError::BlockNotStartingWithBEGIN)?;
                if top.name != end_name {
                    return Err(BlockParseError::MismatchedEndTag {
                        expected: top.name,
                        found: end_name,
                    });
                }
                if let Some(parent) = stack.last_mut() {
                    parent.inner_blocks.push(top);
                } else {
                    root = Some(top);
                }
            } else if let Some(current) = stack.last_mut() {
                current.inner_lines.push(line.into());
            } else if !s.trim().is_empty() {
                return Err(BlockParseError::BlockNotStartingWithBEGIN);
            }
        }

        if let Some(unclosed) = stack.pop() {
            return Err(BlockParseError::UnclosedBlock {
                name: unclosed.name,
            });
        }

        root.ok_or(BlockParseError::BlockNotStartingWithBEGIN)
    }
}

impl TryFrom<&str> for Block {
    type Error = BlockParseError;

    fn try_from(text: &str) -> Result<Self, Self::Error> {
        Self::from_lines(ICalLineParser::new(text))
    }
}

impl FromStr for Block {
    type Err = BlockParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::try_from(s)
    }
}

impl<'a> TryFrom<&'a [String]> for Block {
    type Error = BlockParseError;

    fn try_from(lines: &'a [String]) -> Result<Self, Self::Error> {
        Self::from_lines(lines.iter().map(String::as_str))
    }
}

#[cfg(test)]
#[path = "tests/block.rs"]
mod tests;