tushare_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, DeriveInput, Data, Fields, Type};
4
5/// Derive macro for automatically implementing FromTushareData trait
6/// 
7/// This macro generates the implementation of FromTushareData trait for structs,
8/// enabling automatic conversion from Tushare API response data to Rust structs.
9/// 
10/// # Attributes
11/// 
12/// - `#[tushare(field = "api_field_name")]` - Maps struct field to a different API field name
13/// - `#[tushare(skip)]` - Skips this field during conversion (field must have Default implementation)
14/// - `#[tushare(date_format = "format_string")]` - Specifies custom date format for chrono date/time types
15/// 
16/// # Example
17/// 
18/// ```rust
19/// use tushare_derive::FromTushareData;
20/// 
21/// #[derive(FromTushareData)]
22/// struct Stock {
23///     ts_code: String,
24///     symbol: String,
25///     name: String,
26///     area: Option<String>,
27///     #[tushare(field = "list_date")]
28///     listing_date: Option<String>,
29///     #[tushare(skip)]
30///     calculated_field: f64,
31///     #[tushare(date_format = "%d/%m/%Y")]
32///     custom_date: chrono::NaiveDate,
33/// }
34/// ```
35#[proc_macro_derive(FromTushareData, attributes(tushare))]
36pub fn derive_from_tushare_data(input: TokenStream) -> TokenStream {
37    let input = parse_macro_input!(input as DeriveInput);
38    
39    let name = &input.ident;
40    let fields = match &input.data {
41        Data::Struct(data) => match &data.fields {
42            Fields::Named(fields) => &fields.named,
43            _ => panic!("FromTushareData can only be derived for structs with named fields"),
44        },
45        _ => panic!("FromTushareData can only be derived for structs"),
46    };
47
48    let field_assignments = fields.iter().map(|field| {
49        let field_name = &field.ident;
50        let field_type = &field.ty;
51        
52        // Check for tushare attributes
53        let mut api_field_name = field_name.as_ref().unwrap().to_string();
54        let mut skip_field = false;
55        let mut date_format: Option<String> = None;
56        
57        for attr in &field.attrs {
58            if attr.path().is_ident("tushare") {
59                if let Ok(meta_list) = attr.meta.require_list() {
60                    let tokens_str = meta_list.tokens.to_string();
61                    
62                    // Parse field = "value" pattern
63                    if let Some(field_start) = tokens_str.find("field") {
64                        let after_field = &tokens_str[field_start + 5..]; // Skip "field"
65                        if let Some(eq_pos) = after_field.find('=') {
66                            let after_eq = &after_field[eq_pos + 1..].trim();
67                            if let Some(start_quote) = after_eq.find('"') {
68                                let after_start_quote = &after_eq[start_quote + 1..];
69                                if let Some(end_quote) = after_start_quote.find('"') {
70                                    api_field_name = after_start_quote[..end_quote].to_string();
71                                }
72                            }
73                        }
74                    }
75                    
76                    // Check for skip attribute
77                    if tokens_str.contains("skip") {
78                        skip_field = true;
79                    }
80                    
81                    // Parse date_format = "value" pattern
82                    if let Some(format_start) = tokens_str.find("date_format") {
83                        let after_format = &tokens_str[format_start + 11..]; // Skip "date_format"
84                        if let Some(eq_pos) = after_format.find('=') {
85                            let after_eq = &after_format[eq_pos + 1..].trim();
86                            if let Some(start_quote) = after_eq.find('"') {
87                                let after_start_quote = &after_eq[start_quote + 1..];
88                                if let Some(end_quote) = after_start_quote.find('"') {
89                                    date_format = Some(after_start_quote[..end_quote].to_string());
90                                }
91                            }
92                        }
93                    }
94                }
95            }
96        }
97        
98        if skip_field {
99            quote! {
100                #field_name: Default::default(),
101            }
102        } else {
103            // Generate field assignment using unified trait approach
104            if is_option_type(field_type) {
105                let inner_type = extract_option_inner_type(field_type);
106                
107                if let Some(format) = date_format {
108                    // Use custom date format for optional types
109                    quote! {
110                        #field_name: {
111                            let value = match tushare_api::utils::get_field_value(fields, values, #api_field_name) {
112                                Ok(v) => v,
113                                Err(_) => &serde_json::Value::Null,
114                            };
115                            tushare_api::traits::from_optional_tushare_value_with_date_format::<#inner_type>(value, #format)?
116                        },
117                    }
118                } else {
119                    // Use FromOptionalTushareValue trait for all Option<T> types
120                    quote! {
121                        #field_name: {
122                            let value = match tushare_api::utils::get_field_value(fields, values, #api_field_name) {
123                                Ok(v) => v,
124                                Err(_) => &serde_json::Value::Null,
125                            };
126                            <#inner_type as tushare_api::traits::FromOptionalTushareValue>::from_optional_tushare_value(value)?
127                        },
128                    }
129                }
130            } else {
131                if let Some(format) = date_format {
132                    // Use custom date format for non-optional types
133                    quote! {
134                        #field_name: {
135                            let value = tushare_api::utils::get_field_value(fields, values, #api_field_name)?;
136                            tushare_api::traits::from_tushare_value_with_date_format::<#field_type>(value, #format)?
137                        },
138                    }
139                } else {
140                    // Use FromTushareValue trait for all non-optional types
141                    quote! {
142                        #field_name: {
143                            let value = tushare_api::utils::get_field_value(fields, values, #api_field_name)?;
144                            <#field_type as tushare_api::traits::FromTushareValue>::from_tushare_value(value)?
145                        },
146                    }
147                }
148            }
149        }
150    });
151
152    let expanded = quote! {
153        impl tushare_api::traits::FromTushareData for #name {
154            fn from_row(
155                fields: &[String],
156                values: &[serde_json::Value],
157            ) -> Result<Self, tushare_api::error::TushareError> {
158                Ok(Self {
159                    #(#field_assignments)*
160                })
161            }
162        }
163    };
164
165    TokenStream::from(expanded)
166}
167
168
169
170// Helper functions for type checking
171fn is_option_type(ty: &Type) -> bool {
172    if let Type::Path(type_path) = ty {
173        if let Some(segment) = type_path.path.segments.last() {
174            return segment.ident == "Option";
175        }
176    }
177    false
178}
179
180fn extract_option_inner_type(ty: &Type) -> Type {
181    if let Type::Path(type_path) = ty {
182        if let Some(segment) = type_path.path.segments.last() {
183            if segment.ident == "Option" {
184                if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
185                    if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
186                        return inner_ty.clone();
187                    }
188                }
189            }
190        }
191    }
192    // Fallback to String if we can't extract the inner type
193    syn::parse_str("String").unwrap()
194}
195
196// Note: Type checking functions removed since we now use unified trait calls
197// for all types through FromTushareValue and FromOptionalTushareValue