csv_codegen/
lib.rs

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