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::expression::{FieldExpression, OutputFormat};
use crate::{CsvSource, ElseTemplate, MacroInvocation, PivotSpec, RowTemplate, RowTemplateKind};
use convert_case::Case;
use proc_macro2::Ident;
use std::ops::Bound;
use syn::parse::{Parse, ParseStream};
use syn::token::Paren;
use syn::{Lit, LitStr, RangeLimits, Token, braced, parenthesized};

impl Parse for MacroInvocation {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let header = parse_header(input)?;
        Ok(Self {
            header,
            template: input.parse()?,
        })
    }
}

fn parse_header(parser: ParseStream) -> syn::Result<CsvSource> {
    let from = parser.parse()?;
    let _comma = parser.parse::<Token![,]>()?;

    let pivot = parser
        .peek(super::kw::pivot)
        .then(|| {
            let pivot = parser.parse();
            let _comma = parser.parse::<Token![,]>()?;
            pivot
        })
        .transpose()?;
    Ok(CsvSource { from, pivot })
}

impl Parse for PivotSpec {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let pivot = input.parse()?;
        let content;
        let parens = parenthesized!(content in input);
        let column_from: Option<Lit> = content
            .peek(syn::Lit)
            .then(|| content.parse())
            .transpose()?;
        let range_limits = content.parse()?;
        let column_to: Option<Lit> = content
            .peek(syn::Lit)
            .then(|| content.parse())
            .transpose()?;
        let _comma = content.parse::<Token![,]>()?;
        let key_field_name = content.parse()?;
        let _comma = content.parse::<Token![,]>()?;
        let value_field_name = content.parse()?;

        Ok(Self {
            _kw: pivot,
            _parens: parens,
            column_from,
            _range_limits: range_limits,
            column_to: column_to
                .map(|to| match range_limits {
                    RangeLimits::HalfOpen(_) => Bound::Excluded(to),
                    RangeLimits::Closed(_) => Bound::Included(to),
                })
                .unwrap_or(Bound::Unbounded),
            key_field_name,
            value_field_name,
        })
    }
}

impl Parse for RowTemplate {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let template;

        // Check if it starts with '{' (implicit top-level) or '#' (explicit)
        if input.peek(syn::token::Brace) {
            // Implicit top-level template: { ... }
            let _template_braces = syn::braced!(template in input);
            let template = template.parse()?;

            Ok(Self {
                _hash: syn::parse_quote!(#),  // dummy token
                kind: RowTemplateKind::Plain, // implicit find (expect exactly one)
                filter: None,
                _template_braces,
                template,
                else_template: None,
            })
        } else {
            // Explicit template: #each { ... }, #find(condition) { ... }, or #where(condition) { ... }
            let _hash = input.parse()?;

            // Parse either "each" or "find" keyword
            let kind = if input.peek(crate::kw::each) {
                RowTemplateKind::Each(input.parse()?)
            } else if input.peek(crate::kw::find) {
                RowTemplateKind::Find(input.parse()?)
            } else if input.peek(crate::kw::having) {
                RowTemplateKind::Having(input.parse()?)
            } else {
                return Err(syn::Error::new(
                    input.span(),
                    "Expected 'each', 'find' or 'having'",
                ));
            };

            // Handle filter requirements based on kind
            let filter = if input.peek(syn::token::Paren) {
                Some(input.parse()?)
            } else {
                match kind {
                    RowTemplateKind::Find(_) | RowTemplateKind::Having(_) => {
                        return Err(syn::Error::new(
                            input.span(),
                            "#find or #having requires a condition. Use #find(condition) { ... } or #having(condition) { ... } or use #each for iteration",
                        ));
                    }
                    RowTemplateKind::Each(_) | RowTemplateKind::Plain => None,
                }
            };

            let _template_braces = syn::braced!(template in input);
            let template = template.parse()?;
            let else_template = ElseTemplate::try_parse(input)?;

            if let RowTemplateKind::Having(_) = kind
                && let Some(else_template) = &else_template
            {
                return Err(syn::Error::new_spanned(
                    else_template._else_kw,
                    "#else is invalid following #having",
                ));
            }

            Ok(Self {
                _hash,
                kind,
                filter,
                _template_braces,
                template,
                else_template,
            })
        }
    }
}

impl ElseTemplate {
    fn try_parse(input: ParseStream) -> syn::Result<Option<Self>> {
        if input.peek(Token![#]) && input.peek2(Token![else]) {
            let template;
            Ok(Some(Self {
                _hash: input.parse()?,
                _else_kw: input.parse()?,
                _template_braces: syn::braced!(template in input),
                template: template.parse()?,
            }))
        } else {
            Ok(None)
        }
    }
}

mod kw {
    syn::custom_keyword!(ident);
    syn::custom_keyword!(Type);
    syn::custom_keyword!(CONST);
}

/// call this parse once the outer 2 brackets have already been seen, so we know it should be a select expression
impl Parse for FieldExpression {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let _hash: Token![#] = input.parse()?;
        // #ident()
        if input.peek(kw::ident) {
            let _ident: kw::ident = input.parse()?;
            let syntax_type = OutputFormat::IdentConverted(Case::Snake);
            let content;
            let _ = parenthesized!(content in input);
            let prefix = content
                .peek(syn::Ident)
                .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                .transpose()?
                .unwrap_or_default();
            let field;
            let _ = braced!(field in content);
            let field = field.parse()?;
            let suffix = content
                .peek(syn::Ident)
                .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                .transpose()?
                .unwrap_or_default();
            return Ok(Self {
                field,
                syntax_type,
                prefix,
                suffix,
            });
        }
        // #Type()
        if input.peek(kw::Type) {
            let _ident: kw::Type = input.parse()?;
            let syntax_type = OutputFormat::IdentConverted(Case::Pascal);
            let content;
            let _ = parenthesized!(content in input);
            let prefix = content
                .peek(syn::Ident)
                .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                .transpose()?
                .unwrap_or_default();
            let field;
            let _ = braced!(field in content);
            let field = field.parse()?;
            let suffix = content
                .peek(syn::Ident)
                .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                .transpose()?
                .unwrap_or_default();
            return Ok(Self {
                field,
                syntax_type,
                prefix,
                suffix,
            });
        }
        // #CONST()
        if input.peek(kw::CONST) {
            let _ident: kw::CONST = input.parse()?;
            let syntax_type = OutputFormat::IdentConverted(Case::Constant);
            let content;
            let _ = parenthesized!(content in input);
            let prefix = content
                .peek(syn::Ident)
                .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                .transpose()?
                .unwrap_or_default();
            let field;
            let _ = braced!(field in content);
            let field = field.parse()?;
            let suffix = content
                .peek(syn::Ident)
                .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                .transpose()?
                .unwrap_or_default();
            return Ok(Self {
                field,
                syntax_type,
                prefix,
                suffix,
            });
        }
        // #()
        if input.peek(Paren) {
            let syntax_type = OutputFormat::LitOrIdent;
            let content;
            let _ = parenthesized!(content in input);

            // #("")
            if content.peek(LitStr) {
                let lit_str: LitStr = content.parse()?;
                // todo very sloppy
                let string = lit_str.value();
                let mut split = string.split(['{', '}']);
                let prefix = split.next().unwrap().to_string();
                let mut field: syn::Ident =
                    syn::parse_str(split.next().expect("braces in string"))?;
                field.set_span(lit_str.span());
                let suffix = split.next().unwrap().to_string();
                return Ok(Self {
                    field,
                    syntax_type: OutputFormat::LitStr,
                    prefix,
                    suffix,
                });
            } else {
                let prefix = content
                    .peek(syn::Ident)
                    .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                    .transpose()?
                    .unwrap_or_default();
                let field;
                let _ = braced!(field in content);
                let field = field.parse()?;
                let suffix = content
                    .peek(syn::Ident)
                    .then(|| content.parse::<Ident>().map(|i| i.to_string()))
                    .transpose()?
                    .unwrap_or_default();
                return Ok(Self {
                    field,
                    syntax_type,
                    prefix,
                    suffix,
                });
            }
        }
        Err(syn::Error::new(input.span(), "Expected a replacement"))
    }
}