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 fieldvalue_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” to42_f64
(float literal)#{count}_u32
converts “10” to10_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