csv_codegen/lib.rs
1use predicate::FilterExpression;
2use proc_macro2::TokenStream;
3use std::ops::Bound;
4use syn::token::Paren;
5use syn::{LitStr, RangeLimits, Token};
6use template::TemplateAst;
7
8// Re-export key types from new modules
9pub(crate) use data::{FieldIndex, QueryError, QueryErrorInner};
10
11mod data;
12mod expression;
13mod parse;
14mod pivot_iterator;
15mod predicate;
16mod record_ops;
17mod template;
18mod write;
19
20/// Generates Rust code from CSV data using a templating syntax.
21///
22/// This macro reads a CSV file at compile time and generates code by substituting CSV field values
23/// into a template. It supports filtering, pivoting, and various transformations to convert CSV
24/// data into valid Rust identifiers, literals, and other tokens.
25///
26/// # Syntax
27///
28/// ```ignore
29/// csv_codegen::csv_template!(
30/// "path/to/file.csv",
31/// [pivot(column_range, key_column, value_column),]
32/// {
33/// template_code
34/// }
35/// )
36/// ```
37///
38/// # Arguments
39///
40/// - **CSV path**: Relative path to the file the macro is invoked from, like `include!()`
41/// - **pivot()** (optional): Transforms specified columns into key-value pairs
42/// - `column_range`: Range of columns to pivot (e.g., `5..=9`, `"column_a"..`)
43/// - `key_column`: Name for the generated key field
44/// - `value_column`: Name for the generated value field
45/// - **Template body** : Direct code generation without wrapper (top-level only)
46/// - **#each()** : Iterates over rows, optionally filtering which rows are included
47/// - Can be used at any level with optional conditions
48/// - Condition uses CSV field names and supports `==`, `!=` comparisons
49/// - **#find(condition)** : Finds exactly one matching row, with optional #else fallback
50/// - Always requires a condition to specify which row to find
51/// - Must match exactly one row, compilation error otherwise
52///
53/// # Field Substitution
54///
55/// Fields from the CSV can be substituted into the template using several syntaxes:
56///
57/// ## Identifier transformations
58/// - `#ident(expression)` - Converts to a valid Rust identifier
59/// - Removes spaces, special characters, converts to snake_case
60/// - Example: "Green Apple" → `#ident(get_{field_name}_value)` → `get_green_apple_value`
61/// - `#CONST(expression)` - Converts to a valid Rust constant identifier (SCREAMING_SNAKE_CASE)
62/// - `#Type(expression)` - Converts to a valid Rust type identifier (PascalCase)
63///
64/// ## Literal formatting
65/// - `#({field_name}_suffix)` - Appends suffix to create typed literals
66/// - `#{price}_f64` converts "42" to `42_f64` (float literal)
67/// - `#{count}_u32` converts "10" to `10_u32` (unsigned integer literal)
68/// - `#("{field_name}")` - Format as literal string
69///
70/// # Repetition and Filtering
71///
72/// Use `#each(condition) { template_code }` to repeat template code for each matching row:
73///
74/// ```ignore
75/// #each(status == "active") {
76/// const #CONST({name}): u32 = #({value});
77/// }
78/// ```
79///
80/// The condition can reference any CSV field and supports:
81/// - `==` equality comparison
82/// - `!=` inequality comparison
83/// - String comparisons
84///
85/// # Pivoting
86///
87/// The `pivot()` modifier transforms multiple columns into key-value pairs:
88///
89/// ```rust
90/// let product = "Laptop Stand";
91/// let quarter = "q3_sales";
92/// let sales = csv_codegen::csv_template!("../tests/sales.csv", pivot("q1_sales"..="q4_sales", quarter, amount), {
93/// match (product, quarter) {
94/// #each{
95/// // Each row becomes multiple rows with metric/amount pairs
96/// (#("{product}"), #("{quarter}")) => #({amount}),
97/// }
98/// _ => panic!(),
99/// }
100/// });
101/// assert_eq!(sales, 320);
102/// ```
103///
104/// Given CSV columns `[name, age, height_cm, weight_kg, score_math, score_english]`,
105/// `pivot("height_cm"..="score_english", subject, value)` would create pairs like:
106/// - `subject="height_cm", value="175"`
107/// - `subject="weight_kg", value="70"`
108/// - `subject="score_math", value="95"`
109/// - `subject="score_english", value="88"`
110///
111/// # Examples
112///
113/// ## Basic code generation
114///
115/// ```rust
116/// // CSV: name,price,category
117/// // apple,1,fruit
118/// // carrot,0.80,vegetable
119///
120/// csv_codegen::csv_template!("../tests/products.csv", #each {
121/// pub const #CONST({name}_PRICE): f64 = #({price}_f64);
122/// });
123/// assert_eq!(WIRELESS_HEADPHONES_PRICE, 89.99);
124/// ```
125///
126/// Generates:
127/// ```rust
128/// pub const APPLE_PRICE: f64 = 1_f64;
129/// pub const CARROT_PRICE: f64 = 0.80_f64;
130/// ```
131///
132/// ## Function generation with filtering
133///
134/// ```rust
135/// csv_codegen::csv_template!("../tests/products.csv", {
136/// #each(category == "fruit"){
137/// pub fn #ident(get_{name}_price)() -> f64 {
138/// #({price}_f64)
139/// }
140/// }
141/// })
142/// ```
143///
144/// Generates:
145/// ```rust
146/// pub fn get_apple_price() -> f64 {
147/// 1.20_f64
148/// }
149/// ```
150///
151/// ## Match arms with nested filtering
152///
153/// ```rust
154/// csv_codegen::csv_template!("../tests/products.csv", {
155/// fn get_price(name: &str) -> Option<f64> {
156/// match name {
157/// #each(price != ""){
158/// #("{name}") => Some(#({price}_f64)),
159/// }
160/// _ => None,
161/// }
162/// }
163/// });
164/// assert_eq!(get_price("Ergonomic Chair").unwrap(), 399.99);
165/// ```
166///
167/// ## Pivoting example
168///
169/// ```rust
170/// // CSV: product,q1_sales,q2_sales,q3_sales,q4_sales
171/// // widget,100,150,120,200
172///
173/// csv_codegen::csv_template!("../tests/sales.csv", pivot("q1_sales"..="q4_sales", quarter, sales), {
174/// #each{
175/// struct #Type({product}Product);
176/// impl #Type({product}Product) {
177/// #each{
178/// pub const #CONST({quarter}): u32 = #({sales}_u32);
179/// }
180/// }
181/// }
182/// });
183/// assert_eq!(SmartWatchProduct::Q_2_SALES, 180);
184/// ```
185///
186/// # Notes
187///
188/// - CSV files are read at compile time - changes require recompilation
189/// - Field names are derived from CSV headers
190/// - Empty cells are treated as empty strings
191#[proc_macro]
192pub fn csv_template(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
193 // Parse macro input
194 let input = match syn::parse::<MacroInvocation>(input) {
195 Ok(data) => data,
196 Err(err) => {
197 return err.to_compile_error().into();
198 }
199 };
200
201 // 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
202 let mut errors = TokenStream::new();
203
204 let output = match data::query_csv(&input.header) {
205 Err(err) => {
206 errors.extend(err.into_compile_error());
207 input.template.template.render(None, &mut errors)
208 }
209 Ok(data) => {
210 let for_template = input.template;
211 for_template.render(Some(&data), &mut errors)
212 }
213 };
214
215 // Return the generated code
216 errors.extend(output);
217 errors.into()
218}
219
220struct MacroInvocation {
221 pub header: CsvSource,
222 pub template: RowTemplate,
223}
224
225struct CsvSource {
226 from: LitStr,
227 pivot: Option<PivotSpec>,
228}
229
230mod kw {
231 syn::custom_keyword!(pivot);
232 syn::custom_keyword!(each);
233 syn::custom_keyword!(find);
234}
235
236struct PivotSpec {
237 _kw: kw::pivot,
238 _parens: Paren,
239 column_from: Option<syn::Lit>,
240 _range_limits: RangeLimits,
241 column_to: Bound<syn::Lit>,
242 key_field_name: syn::Ident,
243 value_field_name: syn::Ident,
244}
245
246#[derive(Debug)]
247#[allow(unused)]
248enum RowTemplateKind {
249 Each(kw::each), // #each() - can match 0, 1, or many
250 Find(kw::find), // #find() - must match exactly 1
251 Plain, // Default at top level - must match exactly 1
252}
253
254#[derive(Debug)]
255struct RowTemplate {
256 _hash: Token![#],
257 kind: RowTemplateKind,
258 filter: Option<FilterExpression>,
259 _template_braces: syn::token::Brace,
260 template: TemplateAst,
261 else_template: Option<ElseTemplate>,
262}
263
264#[derive(Debug)]
265struct ElseTemplate {
266 _hash: Token![#],
267 _else_kw: Token![else],
268 _template_braces: syn::token::Brace,
269 template: TemplateAst,
270}