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

§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);

§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