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}