muffy-validation-macro 0.4.0

Macros for document validation in Muffy
Documentation
use crate::{error::MacroError, pattern::Pattern};
use alloc::collections::BTreeSet;
use proc_macro2::TokenStream;
use quote::quote;

const TEXT_TOKEN: &str = "#text";

pub fn generate_content(pattern: &Pattern) -> Result<TokenStream, MacroError> {
    Ok(match pattern {
        Pattern::Attribute(_) => {
            return Err(MacroError::RncPattern("attribute in content pattern"));
        }
        Pattern::NotAllowed => {
            return Err(MacroError::RncPattern("not-allowed content pattern"));
        }
        Pattern::Choice(patterns) => {
            let patterns = generate_contents(patterns)?;

            quote!(Content::Choice(&[#(#patterns),*]))
        }
        Pattern::Element(names) => {
            let names = names.iter().map(|name| quote!(#name));

            quote!(Content::Element(&[#(#names),*]))
        }
        Pattern::Empty => quote!(Content::Empty),
        Pattern::Group(patterns) => {
            let patterns = generate_contents(patterns)?;

            quote!(Content::Group(&[#(#patterns),*]))
        }
        Pattern::Interleave(patterns) => {
            let patterns = generate_contents(patterns)?;

            quote!(Content::Interleave(&[#(#patterns),*]))
        }
        Pattern::Many0(pattern) => {
            let pattern = generate_content(pattern)?;

            quote!(Content::Many0(&#pattern))
        }
        Pattern::Many1(pattern) => {
            let pattern = generate_content(pattern)?;

            quote!(Content::Many1(&#pattern))
        }
        Pattern::Optional(pattern) => {
            let pattern = generate_content(pattern)?;

            quote!(Content::Optional(&#pattern))
        }
        Pattern::Text => quote!(Content::Text),
    })
}

fn generate_contents(patterns: &[Pattern]) -> Result<Vec<TokenStream>, MacroError> {
    patterns.iter().map(generate_content).collect()
}

pub fn children(pattern: &Pattern) -> BTreeSet<String> {
    let mut children = element_names(pattern);

    if contains_text(pattern) {
        children.insert(TEXT_TOKEN.into());
    }

    children
}

fn contains_text(pattern: &Pattern) -> bool {
    match pattern {
        Pattern::Text => true,
        Pattern::Attribute(_) | Pattern::Element(_) | Pattern::Empty | Pattern::NotAllowed => false,
        Pattern::Choice(patterns) | Pattern::Group(patterns) | Pattern::Interleave(patterns) => {
            patterns.iter().any(contains_text)
        }
        Pattern::Many0(pattern) | Pattern::Many1(pattern) | Pattern::Optional(pattern) => {
            contains_text(pattern)
        }
    }
}

fn element_names(pattern: &Pattern) -> BTreeSet<String> {
    match pattern {
        Pattern::Element(names) => names.clone(),
        Pattern::Choice(patterns) | Pattern::Group(patterns) | Pattern::Interleave(patterns) => {
            patterns.iter().flat_map(element_names).collect()
        }
        Pattern::Many0(pattern) | Pattern::Many1(pattern) | Pattern::Optional(pattern) => {
            element_names(pattern)
        }
        Pattern::Attribute(_) | Pattern::Empty | Pattern::NotAllowed | Pattern::Text => {
            Default::default()
        }
    }
}

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

    fn element(name: &str) -> Pattern {
        Pattern::Element([name.into()].into())
    }

    #[test]
    fn compile_ordered_group() {
        assert_eq!(
            generate_content(&Pattern::group([element("foo"), element("bar")]))
                .unwrap()
                .to_string(),
            quote!(Content::Group(&[
                Content::Element(&["foo"]),
                Content::Element(&["bar"])
            ]))
            .to_string()
        );
    }

    #[test]
    fn compile_repetition() {
        assert_eq!(
            generate_content(&Pattern::many0(element("foo")))
                .unwrap()
                .to_string(),
            quote!(Content::Many0(&Content::Element(&["foo"]))).to_string()
        );
    }

    #[test]
    fn collect_children_with_text() {
        assert_eq!(
            children(&Pattern::many0(Pattern::choice([
                Pattern::Text,
                element("foo"),
            ]))),
            ["#text".into(), "foo".into()].into()
        );
    }

    #[test]
    fn fail_on_attribute() {
        assert!(matches!(
            generate_content(&Pattern::Attribute(["foo".into()].into())),
            Err(MacroError::RncPattern(_))
        ));
    }
}