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