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, DataSet, QueryData};
use crate::expression::{FieldExpression, OutputFormat};
use crate::record_ops::FieldResolver;
use crate::{RowTemplate, RowTemplateKind};
use proc_macro2::TokenStream;
use syn::__private::ToTokens;

impl RowTemplate {
    pub(crate) fn render<T>(
        &self,
        data_context: Option<&T>,
        errors: &mut TokenStream,
    ) -> TokenStream
    where
        T: DataSet + FieldResolver,
    {
        if let Some(data_context) = data_context {
            let query = self.template.to_query();
            match data_context.select(self.filter.as_ref(), &query) {
                Ok(tg) => {
                    // Validate cardinality based on template kind
                    match self.kind {
                        RowTemplateKind::Find(_)
                        | RowTemplateKind::Plain
                        | RowTemplateKind::Having(_) => {
                            if let Some(value) =
                                self.render_find(data_context.as_group(), &tg, errors)
                            {
                                return value;
                            }
                        }
                        RowTemplateKind::Each(_) => {
                            // Original behavior for #each
                            if tg.data.is_empty()
                                && let Some(else_template) = &self.else_template
                            {
                                return else_template
                                    .template
                                    .render(Some(&data_context.as_group()), errors);
                            }
                            return tg
                                .data
                                .into_iter()
                                .map(|d| self.template.render(Some(&d), errors))
                                .collect();
                        }
                    }
                }
                Err(err) => {
                    errors.extend(err.to_compile_error());
                }
            }
        }
        self.template.render(None, errors)
    }

    fn render_find(
        &self,
        parent_group: DataGroup,
        data_groups: &QueryData,
        errors: &mut TokenStream,
    ) -> Option<TokenStream> {
        // find output depends on the number of matches
        match data_groups.data.len() {
            // with zero matches, find requires an #else template
            0 => {
                if let RowTemplateKind::Having(_) = self.kind {
                    panic!("#having with no matches should return None to filter out the group");
                }
                if let Some(else_template) = &self.else_template {
                    // the #else template is rendered with the parent group, so parameters from there can be used
                    Some(else_template.template.render(Some(&parent_group), errors))
                } else {
                    // if there's no #else template, and no rows, this is an error
                    let error_msg = if self.filter.is_some() {
                        "#find found no matching rows. Use #else clause or ensure exactly one row matches."
                    } else {
                        "Template expects exactly one group but found none. Use #each for iteration or add #else clause."
                    };
                    errors.extend(
                        syn::Error::new(self._template_braces.span.join(), error_msg)
                            .to_compile_error(),
                    );
                    Some(self.template.render(None, errors))
                }
            }
            1 => {
                // Exactly one match - render it
                Some(self.template.render(Some(&data_groups.data[0]), errors))
            }
            n => {
                let error_msg = if self.filter.is_some() {
                    // Collect information about matching rows for debugging
                    let mut row_info = Vec::new();
                    for group in &data_groups.data {
                        for record in group.record_iter() {
                            // Find the row index in the original data source
                            if let Some(row_idx) = parent_group
                                .source()
                                .record_iter()
                                .position(|r| std::ptr::eq(r, record))
                            {
                                // Get first few field values for debugging
                                let field_values: Vec<String> = record
                                    .iter()
                                    .take(3) // Show first 3 fields
                                    .map(|field| format!("\"{field}\""))
                                    .collect();
                                row_info.push(format!(
                                    "row {}: [{}]",
                                    row_idx + 2,
                                    field_values.join(", ")
                                )); // +2 because CSV rows are 1-indexed and we skip header
                            }
                        }
                    }

                    let row_details = if row_info.len() <= 5 {
                        row_info.join(", ")
                    } else {
                        format!(
                            "{}, ... and {} more",
                            row_info[..5].join(", "),
                            row_info.len() - 5
                        )
                    };

                    format!(
                        "#find found {n} matching rows, expected exactly 1. Matching rows: {row_details}. Use a more specific filter condition to select one row."
                    )
                } else {
                    format!(
                        "Template expects exactly one group but found {n}. Use #each for iteration or add filtering."
                    )
                };
                errors.extend(
                    syn::Error::new(self._template_braces.span.join(), error_msg)
                        .to_compile_error(),
                );
                Some(self.template.render(None, errors))
            }
        }
    }
}

impl FieldExpression {
    pub(crate) fn render(&self, data_source: Option<&DataGroup>) -> TokenStream {
        if let Some(data_source) = data_source {
            data_source.get_field(self)
        } else {
            match self.syntax_type {
                OutputFormat::IdentConverted(_case) => {
                    let ident: syn::Ident =
                        syn::parse_quote!(__placeholder_ident_to_enable_syntax_checking);
                    ident.into_token_stream()
                }
                OutputFormat::LitStr | OutputFormat::LitOrIdent => {
                    let ident: syn::LitStr =
                        syn::parse_quote!("__placeholder_string_to_enable_syntax_checking");
                    ident.into_token_stream()
                }
            }
        }
    }
}