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
//! # csv-codegen
//!
//! A Rust procedural macro for generating code from CSV data at compile time.
//! Transform CSV files into Rust constants, functions, structs, and other code using a flexible templating syntax.
//!
//! ## Features
//!
//! - **Compile-time CSV processing** - CSV files are read and processed during compilation
//! - **Template-based code generation** - Use a simple template syntax to generate any Rust code
//! - **Field transformations** - Convert CSV data to valid Rust identifiers, constants, types, and literals
//! - **Filtering support** - Include/exclude rows based on conditions
//! - **Pivoting** - Transform columns into key-value pairs for more flexible data structures
//! - **Type-safe literals** - Generate properly typed numeric literals (`42_f64`, `10_u32`, etc.)
//!
//! ## Getting Started
//!
//! See the [`csv_template!`] macro documentation for detailed usage instructions, syntax reference, and examples.
//!
//! ## Use Cases
//!
//! - **Configuration from CSV** - Generate constants and enums from configuration data
//! - **Test data** - Create test fixtures from CSV files
//! - **Code tables** - Transform lookup tables into efficient match statements
//! - **Translations** - Create internationalization constants from CSV files
//!
//! ## Additional Resources
//!
//! - [README](../README.md) - Installation and quick start guide
//! - [Integration Tests](../tests/test.rs) - Real-world examples
//! - [Repository](https://git.sr.ht/~platy/csv-codegen) - Source code and issue tracking
//!
//! ## Status
//!
//! This crate is in development and was built with AI assistance (Claude Code).
//! The API may see changes in future versions. Contributions and code review are especially welcome!

use predicate::FilterExpression;
use proc_macro2::TokenStream;
use std::ops::Bound;
use syn::token::Paren;
use syn::{LitStr, RangeLimits, Token};
use template::TemplateAst;

// Re-export key types from new modules
pub(crate) use data::{FieldIndex, QueryError, QueryErrorInner};

mod data;
mod expression;
mod parse;
mod pivot_iterator;
mod predicate;
mod record_ops;
mod template;
mod write;

/// Generates Rust code from CSV data using a templating syntax.
///
/// This macro reads a CSV file at compile time and generates code by substituting CSV field values
/// into a template. It supports filtering, pivoting, and various transformations to convert CSV
/// data into valid Rust identifiers, literals, and other tokens.
///
/// # Syntax
///
/// ```ignore
/// csv_codegen::csv_template!(
///     "path/to/file.csv",
///     [pivot(column_range, key_column, value_column),]
///     {
///         template_code
///     }
/// )
/// ```
///
/// # Arguments
///
/// - **CSV path**: Relative path to the file the macro is invoked from, like `include!()`
/// - **pivot()** (optional): Transforms specified columns into key-value pairs
///   - `column_range`: Range of columns to pivot (e.g., `5..=9`, `"column_a"..`)
///   - `key_column`: Name for the generated key field
///   - `value_column`: Name for the generated value field
/// - **Template body** : Direct code generation without wrapper (top-level only)
/// - **#each()** : Iterates over rows, optionally filtering which rows are included  
///   - Can be used at any level with optional conditions
///   - Condition uses CSV field names and supports `==`, `!=` comparisons
///   - When used without conditions, processes all rows in the current group
/// - **#find(condition)** : Finds exactly one matching row, with optional #else fallback
///   - Always requires a condition to specify which row to find
///   - Must match exactly one row, compilation error otherwise
///   - Creates a context with only the matching row's data
/// - **#having(condition)** : Parent group filtering with single group rendering
///   - Always requires a condition to specify which rows must exist in the parent group
///   - Filters the parent context: only parent groups with matching rows are processed
///   - Internally behaves like #find: expects exactly one group and renders its template once
///   - Cannot be used with #else clauses (if no matching rows, parent group is filtered out)
///   - Similar to SQL HAVING clause - filters groups based on aggregate conditions
///
/// # Field Substitution
///
/// Fields from the CSV can be substituted into the template using several syntaxes:
///
/// ## Identifier transformations
/// - `#ident(expression)` - Converts to a valid Rust identifier
///   - Removes spaces, special characters, converts to snake_case
///   - Example: "Green Apple" → `#ident(get_{field_name}_value)` → `get_green_apple_value`
/// - `#CONST(expression)` - Converts to a valid Rust constant identifier (SCREAMING_SNAKE_CASE)
/// - `#Type(expression)` - Converts to a valid Rust type identifier (PascalCase)
///
/// ## Literal formatting
/// - `#({field_name}_suffix)` - Appends suffix to create typed literals
///   - `#{price}_f64` converts "42" to `42_f64` (float literal)
///   - `#{count}_u32` converts "10" to `10_u32` (unsigned integer literal)
/// - `#("{field_name}")` - Format as literal string
///
/// # Repetition and Filtering
///
/// Use `#each(condition) { template_code }` to repeat template code for each matching row:
///
/// ```ignore
/// #each(status == "active") {
///     const #CONST({name}): u32 = #({value});
/// }
/// ```
///
/// The condition can reference any CSV field and supports:
/// - `==` equality comparison
/// - `!=` inequality comparison  
/// - String comparisons
///
/// # Pivoting
///
/// The `pivot()` modifier transforms multiple columns into key-value pairs:
///
/// ```rust
/// let product = "Laptop Stand";
/// let quarter = "q3_sales";
/// let sales = csv_codegen::csv_template!("../tests/sales.csv", pivot("q1_sales"..="q4_sales", quarter, amount), {
///     match (product, quarter) {
///         #each{
///             // Each row becomes multiple rows with metric/amount pairs
///             (#("{product}"), #("{quarter}")) => #({amount}),
///         }
///         _ => panic!(),
///     }
/// });
/// assert_eq!(sales, 320);
/// ```
///
/// Given CSV columns `[name, age, height_cm, weight_kg, score_math, score_english]`,
/// `pivot("height_cm"..="score_english", subject, value)` would create pairs like:
/// - `subject="height_cm", value="175"`
/// - `subject="weight_kg", value="70"`
/// - `subject="score_math", value="95"`
/// - `subject="score_english", value="88"`
///
/// # Examples
///
/// ## Basic code generation
///
/// ```rust
/// // CSV: name,price,category
/// //      apple,1,fruit
/// //      carrot,0.80,vegetable
///
/// csv_codegen::csv_template!("../tests/products.csv", #each {
///    pub const #CONST({name}_PRICE): f64 = #({price}_f64);
/// });
/// assert_eq!(WIRELESS_HEADPHONES_PRICE, 89.99);
/// ```
///
/// Generates:
/// ```rust
/// pub const APPLE_PRICE: f64 = 1_f64;
/// pub const CARROT_PRICE: f64 = 0.80_f64;
/// ```
///
/// ## Function generation with filtering
///
/// ```rust
/// csv_codegen::csv_template!("../tests/products.csv", {
///     #each(category == "fruit"){
///         pub fn #ident(get_{name}_price)() -> f64 {
///             #({price}_f64)
///         }
///     }
/// })
/// ```
///
/// Generates:
/// ```rust
/// pub fn get_apple_price() -> f64 {
///     1.20_f64
/// }
/// ```
///
/// ## Match arms with nested filtering
///
/// ```rust
/// csv_codegen::csv_template!("../tests/products.csv", {
///     fn get_price(name: &str) -> Option<f64> {
///         match name {
///             #each(price != ""){
///                 #("{name}") => Some(#({price}_f64)),
///             }
///             _ => None,
///         }
///     }
/// });
/// assert_eq!(get_price("Ergonomic Chair").unwrap(), 399.99);
/// ```
///
/// ## Pivoting example
///
/// ```rust
/// // CSV: product,q1_sales,q2_sales,q3_sales,q4_sales
/// //      widget,100,150,120,200
///
/// csv_codegen::csv_template!("../tests/sales.csv", pivot("q1_sales"..="q4_sales", quarter, sales), {
///     #each{
///         struct #Type({product}Product);
///         impl #Type({product}Product) {
///             #each{
///                 pub const #CONST({quarter}): u32 = #({sales}_u32);
///             }
///         }
///     }
/// });
/// assert_eq!(SmartWatchProduct::Q_2_SALES, 180);
/// ```
///
/// ## Group filtering with #having
///
/// The `#having` directive conditionally renders content based on whether the current group
/// contains rows matching a condition. If no rows match, the content is skipped entirely.
/// Internally, it behaves like `#find` but also filters the parent context.
///
/// ```rust,ignore
/// // CSV: department,employee,union_rep,salary
/// //      engineering,alice,false,85000
/// //      engineering,bob,true,90000
/// //      marketing,carol,false,70000
/// //      marketing,dave,false,72000
/// //      sales,eve,true,80000
///
/// csv_codegen::csv_template!("departments.csv", {
///     #each {
///         // Generate department info, but only if department has union representation
///         let dept = (#("{department}"), #having(union_rep == true) {
///             #("{employee}")  // Name of A union rep (behaves like #find internally)
///         });
///     }
/// });
///
/// // Generates tuples for: ("engineering", "bob"), ("sales", "eve")
/// // Marketing department is skipped entirely (no union reps)
/// ```
///
/// **Key insight**: `#having` asks "Does this group have any rows matching the condition?"
/// - If **yes**: renders its template exactly once (like `#find`)  
/// - If **no**: skips the template entirely (filters out the parent group)
/// - Cannot use `#else` because if no rows match, the parent context is filtered out
///
/// **Comparison**:
/// - `#each(union_rep == true)`: Would iterate over each union rep individually
/// - `#find(union_rep == true)`: Would find exactly one union rep or error
/// - `#having(union_rep == true)`: Renders surrounding template if any union reps exist, and finds the union rep within the #having template
///
/// # Notes
///
/// - CSV files are read at compile time - changes require recompilation
/// - Field names are derived from CSV headers
/// - Empty cells are treated as empty strings
#[proc_macro]
pub fn csv_template(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Parse macro input
    let input = match syn::parse::<MacroInvocation>(input) {
        Ok(data) => data,
        Err(err) => {
            return err.to_compile_error().into();
        }
    };

    // Parse csv data, and prepare for application to template, some querying is also done when generating code based on subqueries, this is the global filtering
    let mut errors = TokenStream::new();

    let output = match data::query_csv(&input.header) {
        Err(err) => {
            errors.extend(err.into_compile_error());
            input.template.template.render(None, &mut errors)
        }
        Ok(data) => {
            let for_template = input.template;
            for_template.render(Some(&data), &mut errors)
        }
    };

    // Return the generated code
    errors.extend(output);
    errors.into()
}

struct MacroInvocation {
    pub header: CsvSource,
    pub template: RowTemplate,
}

struct CsvSource {
    from: LitStr,
    pivot: Option<PivotSpec>,
}

mod kw {
    syn::custom_keyword!(pivot);
    syn::custom_keyword!(each);
    syn::custom_keyword!(find);
    syn::custom_keyword!(having);
}

struct PivotSpec {
    _kw: kw::pivot,
    _parens: Paren,
    column_from: Option<syn::Lit>,
    _range_limits: RangeLimits,
    column_to: Bound<syn::Lit>,
    key_field_name: syn::Ident,
    value_field_name: syn::Ident,
}

#[derive(Debug)]
#[allow(unused)]
enum RowTemplateKind {
    Each(kw::each),     // #each() - can match 0, 1, or many
    Find(kw::find),     // #find() - must match exactly 1
    Having(kw::having), // #having() - group must have matching rows, otherwise parent group excluded
    Plain,              // Default at top level - must match exactly 1
}

#[derive(Debug)]
struct RowTemplate {
    _hash: Token![#],
    kind: RowTemplateKind,
    filter: Option<FilterExpression>,
    _template_braces: syn::token::Brace,
    template: TemplateAst,
    else_template: Option<ElseTemplate>,
}

#[derive(Debug)]
struct ElseTemplate {
    _hash: Token![#],
    _else_kw: Token![else],
    _template_braces: syn::token::Brace,
    template: TemplateAst,
}