finance_query_derive/lib.rs
1//! # finance-query-derive
2//!
3//! Procedural macros for the `finance-query` library.
4//!
5//! This crate provides derive macros that automatically generate code for working with
6//! financial data structures, particularly for integration with the Polars DataFrame library.
7//!
8//! ## Features
9//!
10//! - **`ToDataFrame`**: Automatically implement DataFrame conversion for structs
11//!
12//! ## Usage
13//!
14//! This crate is automatically included when you enable the `dataframe` feature in `finance-query`:
15//!
16//! ```toml
17//! [dependencies]
18//! finance-query = { version = "2.0", features = ["dataframe"] }
19//! ```
20//!
21//! ## Example
22//!
23//! ```ignore
24//! use finance_query::ToDataFrame;
25//! use polars::prelude::*;
26//!
27//! #[derive(ToDataFrame)]
28//! struct Quote {
29//! symbol: String,
30//! price: Option<f64>,
31//! volume: Option<i64>,
32//! }
33//!
34//! // Automatically generates:
35//! // - to_dataframe(&self) -> PolarsResult<DataFrame>
36//! // - vec_to_dataframe(&[Self]) -> PolarsResult<DataFrame>
37//!
38//! let quote = Quote {
39//! symbol: "AAPL".to_string(),
40//! price: Some(150.0),
41//! volume: Some(1000000),
42//! };
43//!
44//! let df = quote.to_dataframe()?;
45//! ```
46//!
47//! ## Supported Types
48//!
49//! The `ToDataFrame` derive macro supports the following field types:
50//!
51//! - **Primitives**: `i32`, `i64`, `u32`, `u64`, `f64`, `bool`
52//! - **Strings**: `String`, `Option<String>`
53//! - **Optional primitives**: `Option<i32>`, `Option<f64>`, etc.
54//! - **FormattedValue**: `Option<FormattedValue<f64>>`, `Option<FormattedValue<i64>>`
55//! (automatically extracts the `.raw` field)
56//!
57//! Complex types like nested structs and vectors are automatically skipped and won't
58//! appear in the generated DataFrame.
59//!
60//! ## Generated Methods
61//!
62//! For each struct with `#[derive(ToDataFrame)]`, two methods are generated:
63//!
64//! ### `to_dataframe(&self)`
65//!
66//! Converts a single instance to a one-row DataFrame:
67//!
68//! ```ignore
69//! let quote = Quote { /* ... */ };
70//! let df: DataFrame = quote.to_dataframe()?;
71//! ```
72//!
73//! ### `vec_to_dataframe(items: &[Self])`
74//!
75//! Converts a slice of instances to a multi-row DataFrame:
76//!
77//! ```ignore
78//! let quotes = vec![quote1, quote2, quote3];
79//! let df: DataFrame = Quote::vec_to_dataframe("es)?;
80//! ```
81
82#![warn(missing_docs)]
83#![warn(rustdoc::missing_crate_level_docs)]
84
85use proc_macro::TokenStream;
86use proc_macro2::TokenStream as TokenStream2;
87use quote::quote;
88use syn::{
89 Data, DeriveInput, Fields, GenericArgument, PathArguments, Type, TypePath, parse_macro_input,
90};
91
92/// Derive macro for automatic DataFrame conversion.
93///
94/// Generates a `to_dataframe(&self) -> PolarsResult<DataFrame>` method
95/// that converts all struct fields to DataFrame columns.
96///
97/// # Supported Types
98///
99/// - `String` → String column
100/// - `Option<String>` → nullable String column
101/// - `Option<FormattedValue<f64>>` → extracts `.raw` as `Option<f64>`
102/// - `Option<FormattedValue<i64>>` → extracts `.raw` as `Option<i64>`
103/// - `i32`, `i64`, `f64`, `bool` → direct columns
104/// - `Option<T>` for primitives → nullable columns
105/// - Nested structs/Vec → skipped (complex types not suitable for flat DataFrame)
106///
107/// # Example
108///
109/// ```ignore
110/// #[derive(ToDataFrame)]
111/// pub struct Quote {
112/// pub symbol: String,
113/// pub price: Option<FormattedValue<f64>>,
114/// }
115///
116/// // Generates:
117/// impl Quote {
118/// pub fn to_dataframe(&self) -> PolarsResult<DataFrame> {
119/// df![
120/// "symbol" => [self.symbol.as_str()],
121/// "price" => [self.price.as_ref().and_then(|v| v.raw)],
122/// ]
123/// }
124/// }
125/// ```
126#[proc_macro_derive(ToDataFrame)]
127pub fn derive_to_dataframe(input: TokenStream) -> TokenStream {
128 let input = parse_macro_input!(input as DeriveInput);
129 let name = &input.ident;
130
131 let fields = match &input.data {
132 Data::Struct(data) => match &data.fields {
133 Fields::Named(fields) => &fields.named,
134 _ => {
135 return syn::Error::new_spanned(
136 &input,
137 "ToDataFrame only supports structs with named fields",
138 )
139 .to_compile_error()
140 .into();
141 }
142 },
143 _ => {
144 return syn::Error::new_spanned(&input, "ToDataFrame only supports structs")
145 .to_compile_error()
146 .into();
147 }
148 };
149
150 let mut column_names: Vec<String> = Vec::new();
151 let mut column_values: Vec<TokenStream2> = Vec::new();
152
153 for field in fields.iter() {
154 let field_name = field.ident.as_ref().unwrap();
155 let field_name_str = to_snake_case(&field_name.to_string());
156 let field_type = &field.ty;
157
158 if let Some(value_expr) = generate_column_value(field_name, field_type) {
159 column_names.push(field_name_str);
160 column_values.push(value_expr);
161 }
162 // Skip fields that return None (complex nested types)
163 }
164
165 // Generate vec column value expressions (for vec_to_dataframe)
166 let mut vec_column_values: Vec<TokenStream2> = Vec::new();
167 for field in fields.iter() {
168 let field_name = field.ident.as_ref().unwrap();
169 let field_type = &field.ty;
170
171 if let Some(value_expr) = generate_vec_column_value(field_name, field_type) {
172 vec_column_values.push(value_expr);
173 }
174 }
175
176 let expanded = quote! {
177 #[cfg(feature = "dataframe")]
178 impl #name {
179 /// Converts this struct to a single-row polars DataFrame.
180 ///
181 /// All scalar fields are included as columns. Nested objects
182 /// and complex types are excluded.
183 ///
184 /// This method is auto-generated by the `ToDataFrame` derive macro.
185 pub fn to_dataframe(&self) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
186 use ::polars::prelude::*;
187 df![
188 #( #column_names => #column_values ),*
189 ]
190 }
191
192 /// Converts a slice of structs to a multi-row polars DataFrame.
193 ///
194 /// All scalar fields are included as columns. Nested objects
195 /// and complex types are excluded.
196 ///
197 /// This method is auto-generated by the `ToDataFrame` derive macro.
198 pub fn vec_to_dataframe(items: &[Self]) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
199 use ::polars::prelude::*;
200 df![
201 #( #column_names => #vec_column_values ),*
202 ]
203 }
204 }
205 };
206
207 TokenStream::from(expanded)
208}
209
210/// Converts a field name to snake_case for DataFrame column names.
211fn to_snake_case(s: &str) -> String {
212 s.to_string()
213}
214
215/// Generates the value expression for a DataFrame column based on field type.
216///
217/// Returns `None` for complex types that should be skipped.
218fn generate_column_value(field_name: &syn::Ident, field_type: &Type) -> Option<TokenStream2> {
219 match field_type {
220 // Direct String
221 Type::Path(type_path) if is_string(type_path) => {
222 Some(quote! { [self.#field_name.as_str()] })
223 }
224
225 // Direct FormattedValue<T> - extract .raw
226 Type::Path(type_path) if is_formatted_value(type_path) => {
227 Some(quote! { [self.#field_name.raw] })
228 }
229
230 // Option<T>
231 Type::Path(type_path) if is_option(type_path) => {
232 let inner_type = get_option_inner_type(type_path)?;
233 generate_option_value(field_name, inner_type)
234 }
235
236 // Direct primitives: i32, i64, f64, bool
237 Type::Path(type_path) if is_primitive(type_path) => Some(quote! { [self.#field_name] }),
238
239 // Skip all other types (Vec, nested structs, etc.)
240 _ => None,
241 }
242}
243
244/// Generates the value expression for a DataFrame column when iterating over a Vec.
245///
246/// Returns `None` for complex types that should be skipped.
247fn generate_vec_column_value(field_name: &syn::Ident, field_type: &Type) -> Option<TokenStream2> {
248 match field_type {
249 // Direct String
250 Type::Path(type_path) if is_string(type_path) => {
251 Some(quote! { items.iter().map(|item| item.#field_name.as_str()).collect::<Vec<_>>() })
252 }
253
254 // Direct FormattedValue<T> - extract .raw
255 Type::Path(type_path) if is_formatted_value(type_path) => {
256 Some(quote! { items.iter().map(|item| item.#field_name.raw).collect::<Vec<_>>() })
257 }
258
259 // Option<T>
260 Type::Path(type_path) if is_option(type_path) => {
261 let inner_type = get_option_inner_type(type_path)?;
262 generate_vec_option_value(field_name, inner_type)
263 }
264
265 // Direct primitives: i32, i64, f64, bool
266 Type::Path(type_path) if is_primitive(type_path) => {
267 Some(quote! { items.iter().map(|item| item.#field_name).collect::<Vec<_>>() })
268 }
269
270 // Skip all other types (Vec, nested structs, etc.)
271 _ => None,
272 }
273}
274
275/// Generates value expression for Option<T> fields when iterating over a Vec.
276fn generate_vec_option_value(field_name: &syn::Ident, inner_type: &Type) -> Option<TokenStream2> {
277 match inner_type {
278 // Option<String>
279 Type::Path(type_path) if is_string(type_path) => Some(
280 quote! { items.iter().map(|item| item.#field_name.as_deref()).collect::<Vec<_>>() },
281 ),
282
283 // Option<FormattedValue<T>> - extract .raw
284 Type::Path(type_path) if is_formatted_value(type_path) => Some(
285 quote! { items.iter().map(|item| item.#field_name.as_ref().and_then(|v| v.raw)).collect::<Vec<_>>() },
286 ),
287
288 // Option<primitive>
289 Type::Path(type_path) if is_primitive(type_path) => {
290 Some(quote! { items.iter().map(|item| item.#field_name).collect::<Vec<_>>() })
291 }
292
293 // Skip complex Option<T> types
294 _ => None,
295 }
296}
297
298/// Generates value expression for Option<T> fields.
299fn generate_option_value(field_name: &syn::Ident, inner_type: &Type) -> Option<TokenStream2> {
300 match inner_type {
301 // Option<String>
302 Type::Path(type_path) if is_string(type_path) => {
303 Some(quote! { [self.#field_name.as_deref()] })
304 }
305
306 // Option<FormattedValue<T>> - extract .raw
307 Type::Path(type_path) if is_formatted_value(type_path) => {
308 Some(quote! { [self.#field_name.as_ref().and_then(|v| v.raw)] })
309 }
310
311 // Option<primitive>
312 Type::Path(type_path) if is_primitive(type_path) => Some(quote! { [self.#field_name] }),
313
314 // Skip complex Option<T> types
315 _ => None,
316 }
317}
318
319/// Checks if a type path is `String`.
320fn is_string(type_path: &TypePath) -> bool {
321 type_path
322 .path
323 .segments
324 .last()
325 .map(|seg| seg.ident == "String")
326 .unwrap_or(false)
327}
328
329/// Checks if a type path is `Option<T>`.
330fn is_option(type_path: &TypePath) -> bool {
331 type_path
332 .path
333 .segments
334 .last()
335 .map(|seg| seg.ident == "Option")
336 .unwrap_or(false)
337}
338
339/// Checks if a type path is `FormattedValue<T>`.
340fn is_formatted_value(type_path: &TypePath) -> bool {
341 type_path
342 .path
343 .segments
344 .last()
345 .map(|seg| seg.ident == "FormattedValue")
346 .unwrap_or(false)
347}
348
349/// Checks if a type path is a primitive type (i32, i64, f64, bool).
350fn is_primitive(type_path: &TypePath) -> bool {
351 type_path
352 .path
353 .segments
354 .last()
355 .map(|seg| {
356 let name = seg.ident.to_string();
357 matches!(
358 name.as_str(),
359 "i32" | "i64" | "f64" | "bool" | "u32" | "u64"
360 )
361 })
362 .unwrap_or(false)
363}
364
365/// Extracts the inner type from Option<T>.
366fn get_option_inner_type(type_path: &TypePath) -> Option<&Type> {
367 let segment = type_path.path.segments.last()?;
368 if segment.ident != "Option" {
369 return None;
370 }
371
372 match &segment.arguments {
373 PathArguments::AngleBracketed(args) => args.args.first().and_then(|arg| {
374 if let GenericArgument::Type(ty) = arg {
375 Some(ty)
376 } else {
377 None
378 }
379 }),
380 _ => None,
381 }
382}