finance_query_derive/
lib.rs1#![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, GenericParam, Ident, PathArguments, Type,
90 TypeParam, TypeParamBound, TypePath, parse_macro_input,
91};
92
93#[proc_macro_derive(ToDataFrame)]
128pub fn derive_to_dataframe(input: TokenStream) -> TokenStream {
129 let input = parse_macro_input!(input as DeriveInput);
130 let name = &input.ident;
131
132 let fields = match &input.data {
133 Data::Struct(data) => match &data.fields {
134 Fields::Named(fields) => &fields.named,
135 _ => {
136 return syn::Error::new_spanned(
137 &input,
138 "ToDataFrame only supports structs with named fields",
139 )
140 .to_compile_error()
141 .into();
142 }
143 },
144 _ => {
145 return syn::Error::new_spanned(&input, "ToDataFrame only supports structs")
146 .to_compile_error()
147 .into();
148 }
149 };
150
151 let has_format_param = input.generics.params.iter().any(|param| {
153 if let GenericParam::Type(TypeParam { bounds, .. }) = param {
154 bounds.iter().any(|b| {
155 if let TypeParamBound::Trait(tb) = b {
156 tb.path
157 .segments
158 .last()
159 .map(|s| s.ident == "Format")
160 .unwrap_or(false)
161 } else {
162 false
163 }
164 })
165 } else {
166 false
167 }
168 });
169
170 let impl_ty = if has_format_param {
171 quote! { #name<crate::format::Both> }
172 } else {
173 quote! { #name }
174 };
175
176 let format_param_ident: Option<&Ident> = input.generics.params.iter().find_map(|param| {
178 if let GenericParam::Type(TypeParam { ident, bounds, .. }) = param {
179 let is_format = bounds.iter().any(|b| {
180 if let TypeParamBound::Trait(tb) = b {
181 tb.path
182 .segments
183 .last()
184 .map(|s| s.ident == "Format")
185 .unwrap_or(false)
186 } else {
187 false
188 }
189 });
190 if is_format { Some(ident) } else { None }
191 } else {
192 None
193 }
194 });
195
196 let mut column_names: Vec<String> = Vec::new();
197 let mut column_values: Vec<TokenStream2> = Vec::new();
198
199 for field in fields.iter() {
200 let field_name = field.ident.as_ref().unwrap();
201 let field_name_str = to_snake_case(&field_name.to_string());
202 let field_type = &field.ty;
203
204 if let Some(value_expr) = generate_column_value(field_name, field_type, format_param_ident)
205 {
206 column_names.push(field_name_str);
207 column_values.push(value_expr);
208 }
209 }
210
211 let mut vec_column_values: Vec<TokenStream2> = Vec::new();
212 for field in fields.iter() {
213 let field_name = field.ident.as_ref().unwrap();
214 let field_type = &field.ty;
215
216 if let Some(value_expr) =
217 generate_vec_column_value(field_name, field_type, format_param_ident)
218 {
219 vec_column_values.push(value_expr);
220 }
221 }
222
223 let expanded = quote! {
224 #[cfg(feature = "dataframe")]
225 impl #impl_ty {
226 pub fn to_dataframe(&self) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
233 use ::polars::prelude::*;
234 df![
235 #( #column_names => #column_values ),*
236 ]
237 }
238
239 pub fn vec_to_dataframe(items: &[Self]) -> ::polars::prelude::PolarsResult<::polars::prelude::DataFrame> {
246 use ::polars::prelude::*;
247 df![
248 #( #column_names => #vec_column_values ),*
249 ]
250 }
251 }
252 };
253
254 TokenStream::from(expanded)
255}
256
257fn to_snake_case(s: &str) -> String {
259 s.to_string()
260}
261
262fn generate_column_value(
266 field_name: &syn::Ident,
267 field_type: &Type,
268 fmt_param: Option<&Ident>,
269) -> Option<TokenStream2> {
270 match field_type {
271 Type::Path(type_path) if is_string(type_path) => {
272 Some(quote! { [self.#field_name.as_str()] })
273 }
274 Type::Path(type_path) if is_formatted_value(type_path) => {
275 Some(quote! { [self.#field_name.raw] })
276 }
277 Type::Path(type_path) if is_option(type_path) => {
278 let inner_type = get_option_inner_type(type_path)?;
279 generate_option_value(field_name, inner_type, fmt_param)
280 }
281 Type::Path(type_path) if is_primitive(type_path) => Some(quote! { [self.#field_name] }),
282 _ => None,
283 }
284}
285
286fn generate_vec_column_value(
290 field_name: &syn::Ident,
291 field_type: &Type,
292 fmt_param: Option<&Ident>,
293) -> Option<TokenStream2> {
294 match field_type {
295 Type::Path(type_path) if is_string(type_path) => {
296 Some(quote! { items.iter().map(|item| item.#field_name.as_str()).collect::<Vec<_>>() })
297 }
298 Type::Path(type_path) if is_formatted_value(type_path) => {
299 Some(quote! { items.iter().map(|item| item.#field_name.raw).collect::<Vec<_>>() })
300 }
301 Type::Path(type_path) if is_option(type_path) => {
302 let inner_type = get_option_inner_type(type_path)?;
303 generate_vec_option_value(field_name, inner_type, fmt_param)
304 }
305 Type::Path(type_path) if is_primitive(type_path) => {
306 Some(quote! { items.iter().map(|item| item.#field_name).collect::<Vec<_>>() })
307 }
308 _ => None,
309 }
310}
311
312fn generate_vec_option_value(
314 field_name: &syn::Ident,
315 inner_type: &Type,
316 fmt_param: Option<&Ident>,
317) -> Option<TokenStream2> {
318 match inner_type {
319 Type::Path(type_path) if is_string(type_path) => Some(
320 quote! { items.iter().map(|item| item.#field_name.as_deref()).collect::<Vec<_>>() },
321 ),
322 Type::Path(type_path)
324 if is_formatted_value(type_path) || is_format_assoc_value(type_path, fmt_param) =>
325 {
326 Some(
327 quote! { items.iter().map(|item| item.#field_name.as_ref().and_then(|v| v.raw)).collect::<Vec<_>>() },
328 )
329 }
330 Type::Path(type_path) if is_primitive(type_path) => {
331 Some(quote! { items.iter().map(|item| item.#field_name).collect::<Vec<_>>() })
332 }
333 _ => None,
334 }
335}
336
337fn generate_option_value(
339 field_name: &syn::Ident,
340 inner_type: &Type,
341 fmt_param: Option<&Ident>,
342) -> Option<TokenStream2> {
343 match inner_type {
344 Type::Path(type_path) if is_string(type_path) => {
345 Some(quote! { [self.#field_name.as_deref()] })
346 }
347 Type::Path(type_path)
349 if is_formatted_value(type_path) || is_format_assoc_value(type_path, fmt_param) =>
350 {
351 Some(quote! { [self.#field_name.as_ref().and_then(|v| v.raw)] })
352 }
353 Type::Path(type_path) if is_primitive(type_path) => Some(quote! { [self.#field_name] }),
354 _ => None,
355 }
356}
357
358fn is_string(type_path: &TypePath) -> bool {
360 type_path
361 .path
362 .segments
363 .last()
364 .map(|seg| seg.ident == "String")
365 .unwrap_or(false)
366}
367
368fn is_option(type_path: &TypePath) -> bool {
370 type_path
371 .path
372 .segments
373 .last()
374 .map(|seg| seg.ident == "Option")
375 .unwrap_or(false)
376}
377
378fn is_formatted_value(type_path: &TypePath) -> bool {
380 type_path
381 .path
382 .segments
383 .last()
384 .map(|seg| seg.ident == "FormattedValue")
385 .unwrap_or(false)
386}
387
388fn is_format_assoc_value(type_path: &TypePath, fmt_param: Option<&Ident>) -> bool {
394 let Some(param) = fmt_param else { return false };
395 let segs = &type_path.path.segments;
396 segs.len() == 2 && segs[0].ident == *param && segs[1].ident == "Value"
397}
398
399fn is_primitive(type_path: &TypePath) -> bool {
401 type_path
402 .path
403 .segments
404 .last()
405 .map(|seg| {
406 let name = seg.ident.to_string();
407 matches!(
408 name.as_str(),
409 "i32" | "i64" | "f64" | "bool" | "u32" | "u64"
410 )
411 })
412 .unwrap_or(false)
413}
414
415#[proc_macro_derive(FormatConvert)]
427pub fn derive_format_convert(input: TokenStream) -> TokenStream {
428 let input = parse_macro_input!(input as DeriveInput);
429 let name = &input.ident;
430
431 let fields = match &input.data {
432 Data::Struct(data) => match &data.fields {
433 Fields::Named(fields) => &fields.named,
434 _ => {
435 return syn::Error::new_spanned(
436 &input,
437 "FormatConvert only supports structs with named fields",
438 )
439 .to_compile_error()
440 .into();
441 }
442 },
443 _ => {
444 return syn::Error::new_spanned(&input, "FormatConvert only supports structs")
445 .to_compile_error()
446 .into();
447 }
448 };
449
450 let format_param: Option<&Ident> = input.generics.params.iter().find_map(|param| {
452 if let GenericParam::Type(TypeParam { ident, bounds, .. }) = param {
453 let has_format = bounds.iter().any(|b| {
454 if let TypeParamBound::Trait(tb) = b {
455 tb.path
456 .segments
457 .last()
458 .map(|s| s.ident == "Format")
459 .unwrap_or(false)
460 } else {
461 false
462 }
463 });
464 if has_format { Some(ident) } else { None }
465 } else {
466 None
467 }
468 });
469
470 let Some(format_param) = format_param else {
471 return syn::Error::new_spanned(
472 &input,
473 "FormatConvert requires a generic param bounded by Format (e.g. <F: Format = Both>)",
474 )
475 .to_compile_error()
476 .into();
477 };
478
479 let classified: Vec<(&syn::Field, bool)> = fields
481 .iter()
482 .map(|f| (f, is_format_value_field(&f.ty, format_param)))
483 .collect();
484
485 let raw_field_exprs: Vec<_> = classified
487 .iter()
488 .map(|(f, is_fmt)| {
489 let ident = f.ident.as_ref().unwrap();
490 if *is_fmt {
491 quote! { #ident: v.#ident.and_then(|fv| fv.raw) }
492 } else {
493 quote! { #ident: v.#ident }
494 }
495 })
496 .collect();
497
498 let pretty_field_exprs: Vec<_> = classified
499 .iter()
500 .map(|(f, is_fmt)| {
501 let ident = f.ident.as_ref().unwrap();
502 if *is_fmt {
503 quote! { #ident: v.#ident.and_then(|fv| fv.fmt.or(fv.long_fmt)) }
504 } else {
505 quote! { #ident: v.#ident }
506 }
507 })
508 .collect();
509
510 let expanded = quote! {
511 impl From<#name<crate::format::Both>> for #name<crate::format::Raw> {
512 fn from(v: #name<crate::format::Both>) -> Self {
513 #name {
514 #(#raw_field_exprs,)*
515 }
516 }
517 }
518
519 impl From<#name<crate::format::Both>> for #name<crate::format::Pretty> {
520 fn from(v: #name<crate::format::Both>) -> Self {
521 #name {
522 #(#pretty_field_exprs,)*
523 }
524 }
525 }
526
527 impl #name<crate::format::Both> {
528 pub fn into_raw(self) -> #name<crate::format::Raw> { self.into() }
530 pub fn as_raw(&self) -> #name<crate::format::Raw> { self.clone().into() }
532 pub fn into_pretty(self) -> #name<crate::format::Pretty> { self.into() }
534 pub fn as_pretty(&self) -> #name<crate::format::Pretty> { self.clone().into() }
536 }
537 };
538
539 TokenStream::from(expanded)
540}
541
542fn is_format_value_field(ty: &Type, format_param: &Ident) -> bool {
544 let inner = match get_option_inner(ty) {
545 Some(t) => t,
546 None => return false,
547 };
548 if let Type::Path(tp) = inner {
550 let segs = &tp.path.segments;
551 if segs.len() == 2 {
552 return segs[0].ident == *format_param && segs[1].ident == "Value";
553 }
554 }
555 false
556}
557
558fn get_option_inner(ty: &Type) -> Option<&Type> {
560 if let Type::Path(tp) = ty {
561 let seg = tp.path.segments.last()?;
562 if seg.ident != "Option" {
563 return None;
564 }
565 if let PathArguments::AngleBracketed(args) = &seg.arguments {
566 return args.args.first().and_then(|a| {
567 if let GenericArgument::Type(t) = a {
568 Some(t)
569 } else {
570 None
571 }
572 });
573 }
574 }
575 None
576}
577
578fn get_option_inner_type(type_path: &TypePath) -> Option<&Type> {
580 let segment = type_path.path.segments.last()?;
581 if segment.ident != "Option" {
582 return None;
583 }
584
585 match &segment.arguments {
586 PathArguments::AngleBracketed(args) => args.args.first().and_then(|arg| {
587 if let GenericArgument::Type(ty) = arg {
588 Some(ty)
589 } else {
590 None
591 }
592 }),
593 _ => None,
594 }
595}