Macro csv_template

Source
csv_template!() { /* proc-macro */ }
Expand description

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

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:

#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:

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

// 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:

pub const APPLE_PRICE: f64 = 1_f64;
pub const CARROT_PRICE: f64 = 0.80_f64;

§Function generation with filtering

csv_codegen::csv_template!("../tests/products.csv", {
    #each(category == "fruit"){
        pub fn #ident(get_{name}_price)() -> f64 {
            #({price}_f64)
        }
    }
})

Generates:

pub fn get_apple_price() -> f64 {
    1.20_f64
}

§Match arms with nested filtering

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

// 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.

// 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