csv-codegen 0.2.3

A Rust procedural macro that transforms CSV data into safe, zero-cost code. Generate match arms, loops, and nested queries directly from CSV files, ensuring type safety and deterministic code generation.
Documentation
use crate::data::{DataGroup, Query};
use crate::expression::FieldExpression;
use crate::{RowTemplate, RowTemplateKind, kw};
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
use syn::Token;
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};

#[derive(Debug)]
pub struct TemplateAst(Vec<TemplatedTokenTree>);

#[derive(Debug)]
pub enum TemplatedTokenTree {
    Group(TemplatedGroup),
    Ident(proc_macro2::Ident),
    Punct(proc_macro2::Punct),
    Literal(proc_macro2::Literal),
    Replacement(Replacement),
    Error(syn::Error),
}

#[derive(Debug)]
pub struct TemplatedGroup {
    delimiter: Delimiter,
    stream: TemplateAst,
}

#[derive(Debug)]
pub enum Replacement {
    Expression(FieldExpression),
    Group(RowTemplate),
}

impl TemplateAst {
    /// If there's an error eg. the data can't be loaded, we still output everything in the template as best we can, inorder to allow the compiler to be as helpful as possible, anythignm we output can be checked
    pub(crate) fn render(
        &self,
        data_source: Option<&DataGroup>,
        errors: &mut TokenStream,
    ) -> TokenStream {
        let mut output = TokenStream::new();
        for tree in &self.0 {
            match tree {
                TemplatedTokenTree::Group(group) => output.extend([TokenTree::Group(
                    group.to_group_for_data(data_source, errors),
                )]),
                TemplatedTokenTree::Ident(ident) => {
                    output.extend([TokenTree::Ident(ident.clone())])
                }
                TemplatedTokenTree::Punct(punct) => {
                    output.extend([TokenTree::Punct(punct.clone())])
                }
                TemplatedTokenTree::Literal(literal) => {
                    output.extend([TokenTree::Literal(literal.clone())])
                }
                TemplatedTokenTree::Replacement(replacement) => {
                    output.extend(replacement.render(data_source, errors))
                }
                TemplatedTokenTree::Error(error) => errors.extend(error.to_compile_error()),
            }
        }
        output
    }
}

impl TemplatedGroup {
    pub(crate) fn to_group_for_data(
        &self,
        data_source: Option<&DataGroup>,
        errors: &mut TokenStream,
    ) -> Group {
        Group::new(self.delimiter, self.stream.render(data_source, errors))
    }
}

impl Replacement {
    pub(crate) fn render(
        &self,
        data_source: Option<&DataGroup>,
        errors: &mut TokenStream,
    ) -> TokenStream {
        match self {
            Replacement::Expression(expr) => expr.render(data_source),
            Replacement::Group(group) => group.render(data_source, errors),
        }
    }
}

impl TemplatedGroup {
    fn new(delimiter: Delimiter, token_stream: TokenStream) -> TemplatedGroup {
        Self {
            delimiter,
            stream: syn::parse2(token_stream).expect("TTS parse never fails"),
        }
    }
}

fn try_parse_replacement<'a>(
    hash: &TokenTree,
    cursor: Cursor<'a>,
) -> Option<syn::Result<(Replacement, Cursor<'a>)>> {
    let (second, rest) = cursor.token_tree()?;
    match &second {
        // #each() {} or #each {} or #find() {} or #find {} or #filter() {}
        TokenTree::Ident(ident) if ident == "each" || ident == "find" || ident == "having" => {
            let keyword = second;
            let mut tokens = vec![hash.clone(), keyword];

            let (third_token, rest) = rest.token_tree()?;

            // If third token is parens, add it and get the fourth token (braces)
            let rest = if let TokenTree::Group(group) = &third_token {
                if group.delimiter() == Delimiter::Parenthesis {
                    tokens.push(third_token);
                    let (fourth_token, rest) = rest.token_tree()?;
                    tokens.push(fourth_token);
                    rest
                } else {
                    // Third token is braces (no filter)
                    tokens.push(third_token);
                    rest
                }
            } else {
                return None; // Invalid syntax
            };

            // Check for #else clause
            if let Some((else_hash, rest)) = rest.token_tree()
                && let TokenTree::Punct(p) = &else_hash
                && p.as_char() == '#'
            {
                let (r#else, rest) = rest.token_tree()?;
                if &r#else.to_string() == "else" {
                    let (else_template_braces, rest) = rest.token_tree()?;
                    tokens.extend([else_hash, r#else, else_template_braces]);

                    let result = syn::parse2(tokens.into_iter().collect());
                    return Some(result.map(|replacement| (replacement, rest)));
                }
            }

            let result = syn::parse2(tokens.into_iter().collect());
            Some(result.map(|replacement| (replacement, rest)))
        }
        // #ident()
        TokenTree::Ident(_ident) => {
            let ident = second;
            let (template_parens, rest) = rest.token_tree()?;
            let result = syn::parse2([hash.clone(), ident, template_parens].into_iter().collect());
            Some(result.map(|replacement| (replacement, rest)))
        }
        // #()
        TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
            let result = syn::parse2([hash.clone(), second].into_iter().collect());
            Some(result.map(|replacement| (replacement, rest)))
        }
        // #""
        TokenTree::Literal(_literal) => {
            let result = syn::parse2([hash.clone(), second].into_iter().collect());
            Some(result.map(|replacement| (replacement, rest)))
        }
        _ => None,
    }
}

impl Parse for TemplateAst {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut errors = vec![];
        let mut templated = vec![];
        input
            .step(|cursor| {
                let mut rest = *cursor;
                while let Some((tt, next)) = rest.token_tree() {
                    if let TokenTree::Punct(p) = &tt
                        && p.as_char() == '#'
                        && let Some(result) = try_parse_replacement(&tt, next)
                    {
                        match result {
                            Ok((replacement, cursor)) => {
                                rest = cursor;
                                templated.push(TemplatedTokenTree::Replacement(replacement));
                                continue;
                            }
                            Err(error) => {
                                errors.push(error);
                            }
                        }
                    }
                    templated.push(tt.into());
                    rest = next;
                }
                Ok(((), rest))
            })
            .expect("No failure case");
        for error in errors {
            templated.push(TemplatedTokenTree::Error(error));
        }
        Ok(Self(templated))
    }
}

impl From<TokenTree> for TemplatedTokenTree {
    fn from(token_tree: TokenTree) -> Self {
        match token_tree {
            TokenTree::Group(group) => {
                Self::Group(TemplatedGroup::new(group.delimiter(), group.stream()))
            }
            TokenTree::Ident(ident) => Self::Ident(ident),
            TokenTree::Punct(punct) => Self::Punct(punct),
            TokenTree::Literal(literal) => Self::Literal(literal),
        }
    }
}

impl Parse for Replacement {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        assert!(input.peek(Token![#]));
        if input.peek2(kw::each) || input.peek2(kw::find) || input.peek2(kw::having) {
            Ok(Replacement::Group(input.parse()?))
        } else {
            Ok(Replacement::Expression(input.parse()?))
        }
    }
}

impl TemplateAst {
    pub(crate) fn to_query(&self) -> Query {
        self.0.iter().map(|tree| tree.to_query()).collect()
    }
}

impl TemplatedTokenTree {
    pub(crate) fn to_query(&self) -> Query {
        match self {
            TemplatedTokenTree::Group(group) => group.stream.to_query(),
            TemplatedTokenTree::Ident(_)
            | TemplatedTokenTree::Punct(_)
            | TemplatedTokenTree::Literal(_)
            | TemplatedTokenTree::Error(_) => Query::default(),
            TemplatedTokenTree::Replacement(replacement) => replacement.to_query(),
        }
    }
}

impl Replacement {
    fn to_query(&self) -> Query {
        match self {
            Replacement::Expression(expr) => expr.to_query(),
            Replacement::Group(group) => group.to_query(),
        }
    }
}

impl RowTemplate {
    fn to_query(&self) -> Query {
        if let RowTemplateKind::Having(_) = self.kind
            && let Some(filter) = &self.filter
        {
            Query::any(filter.clone())
        } else {
            self.else_template
                .as_ref()
                .map_or_else(Query::default, |et| et.template.to_query())
        }
    }
}

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

    #[test]
    fn parse_str_replacement() {
        let r = syn::parse_str::<Replacement>(r#" #("{field}") "#).unwrap();
        let mut errors = TokenStream::new();
        let output = r.render(None, &mut errors);
        assert_snapshot!(output.to_string(), @r#""__placeholder_string_to_enable_syntax_checking""#);
    }

    #[test]
    fn parse_literal_replacement() {
        let r = syn::parse_str::<Replacement>(r#"#({field})"#).unwrap();
        let mut errors = TokenStream::new();
        let output = r.render(None, &mut errors);
        assert_snapshot!(output.to_string(), @r#""__placeholder_string_to_enable_syntax_checking""#);
    }

    #[test]
    fn parse_literal_replacement_in_template_stream() {
        let r = syn::parse_str::<TemplateAst>(r#"let #({field});"#).unwrap();
        let mut errors = TokenStream::new();
        let output = r.render(None, &mut errors);
        assert_snapshot!(output.to_string(), @r#"let "__placeholder_string_to_enable_syntax_checking" ;"#);
    }

    #[test]
    fn parse_for_loop_replacement() {
        let r = syn::parse_str::<Replacement>(
            r#"#each(category == "citrus"){
            match fruit_id {
                #each{
                    #({id}) => concat!("Found citrus: ", #("{name}")),
                }
                _ => "Not a citrus fruit",
            }
        }"#,
        )
        .unwrap();
        let mut errors = TokenStream::new();
        let output = r.render(None, &mut errors);
        assert_snapshot!(output.to_string(), @r#"match fruit_id { "__placeholder_string_to_enable_syntax_checking" => concat ! ("Found citrus: " , "__placeholder_string_to_enable_syntax_checking") , _ => "Not a citrus fruit" , }"#);
    }
}