io_excel/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    punctuated::Punctuated, spanned::Spanned, DataStruct, DeriveInput, Field, FieldsNamed, Ident,
5    Token,
6};
7
8#[proc_macro_derive(IOExcel, attributes(column))]
9pub fn excel_io_derive(input: TokenStream) -> TokenStream {
10    let st = syn::parse_macro_input!(input as DeriveInput);
11    match do_expand(&st) {
12        Ok(tokens) => tokens.into(),
13        Err(err) => err.to_compile_error().into(),
14    }
15}
16
17fn do_expand(st: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
18    let struct_ident = &st.ident;
19    let excel_struct_ident = get_excel_struct_ident(struct_ident);
20
21    let struct_fields = get_struct_fields(st)?;
22
23    let excel_fields = parse_excel_io_stream(struct_fields)?;
24
25    let from_fields = parse_from_excel_to_record_stream(struct_fields)?;
26
27    let write_token_stream = parse_write_fields_stream(struct_fields)?;
28
29    let token_stream = quote! {
30        use calamine::Reader;
31        #[derive(Debug, serde::Deserialize)]
32        pub struct #excel_struct_ident{
33            #(#excel_fields),*
34        }
35
36        impl #struct_ident {
37            pub fn read_excel(file_path: &str,sheet: &str) -> std::result::Result<std::vec::Vec<#struct_ident>, std::boxed::Box<dyn std::error::Error>> {
38                let mut workbook = calamine::open_workbook_auto(file_path)?;
39
40                let range = workbook.worksheet_range(sheet).unwrap();
41
42                let mut result_list = std::vec::Vec::new();
43
44                let deserializer = calamine::RangeDeserializerBuilder::new().from_range(&range)?;
45                for result in deserializer {
46                    let record: #excel_struct_ident = result?;
47                    let record: #struct_ident = record.into();
48                    result_list.push(record);
49                }
50                Ok(result_list)
51            }
52
53            pub fn write_excel(file_path: &str,sheet: &str,record_list:&[#struct_ident]) -> std::result::Result<(), std::boxed::Box<dyn std::error::Error>> {
54                let mut workbook = rust_xlsxwriter::Workbook::new();
55                let worksheet = workbook.add_worksheet();
56                worksheet.set_name(sheet)?;
57                #write_token_stream
58                workbook.save(file_path).unwrap();
59                Ok(())
60            }
61        }
62
63        impl From<#excel_struct_ident> for #struct_ident{
64            fn from(record:#excel_struct_ident) -> Self {
65                Self{
66                    #(#from_fields),*
67                }
68            }
69        }
70    };
71    Ok(token_stream)
72}
73
74fn parse_excel_io_stream(
75    struct_fields: &StructFields,
76) -> syn::Result<Vec<proc_macro2::TokenStream>> {
77    let mut token_stream_list = Vec::new();
78    for field in struct_fields {
79        let excel_field = get_io_excel_field(field)?;
80        let ident = excel_field.ident;
81        let ty = excel_field.ty;
82        let column = excel_field.column_name.unwrap();
83        let token = quote! {
84            #[serde(rename = #column)]
85            pub #ident: #ty
86        };
87        token_stream_list.push(token);
88    }
89    Ok(token_stream_list)
90}
91
92fn parse_from_excel_to_record_stream(
93    struct_fields: &StructFields,
94) -> syn::Result<Vec<proc_macro2::TokenStream>> {
95    let mut token_stream_list = Vec::new();
96    for field in struct_fields {
97        let excel_field = get_io_excel_field(field)?;
98        let ident = excel_field.ident;
99        let token = quote! {
100            #ident:record.#ident
101        };
102        token_stream_list.push(token);
103    }
104    Ok(token_stream_list)
105}
106
107fn parse_write_fields_stream(
108    struct_fields: &StructFields,
109) -> syn::Result<proc_macro2::TokenStream> {
110    let mut header_token_stream = proc_macro2::TokenStream::new();
111
112    let mut row_token_stream = proc_macro2::TokenStream::new();
113
114    for (row, field) in struct_fields.iter().enumerate() {
115        let excel_field = get_io_excel_field(field)?;
116        let ident = excel_field.ident;
117        let header = excel_field.column_name.unwrap();
118
119        let header_token = quote! {
120            worksheet.write_string(0, #row as u16, #header)?;
121        };
122
123        header_token_stream.extend(header_token);
124
125        if excel_field.is_option {
126            let row_token = quote! {
127                let val_str = record.#ident.as_ref().map(|n| format!("{}", n)).unwrap_or_default();
128                worksheet.write_string(row_idx as u32 + 1, #row as u16, &val_str)?;
129            };
130            row_token_stream.extend(row_token);
131        } else {
132            let row_token = quote! {
133                let val = format!("{}",record.#ident);
134                worksheet.write_string(row_idx as u32 + 1, #row as u16, &val)?;
135            };
136            row_token_stream.extend(row_token);
137        }
138    }
139    let token_stream = quote! {
140        #header_token_stream
141        for (row_idx, record) in record_list.iter().enumerate() {
142            #row_token_stream
143        }
144    };
145    Ok(token_stream)
146}
147
148fn get_excel_struct_ident(struct_ident: &Ident) -> syn::Ident {
149    let struct_literal = struct_ident.to_string();
150    let excel_struct_literal = format!("{}IOExcel", struct_literal);
151    syn::Ident::new(&excel_struct_literal, struct_ident.span())
152}
153
154type StructFields = Punctuated<Field, Token![,]>;
155fn get_struct_fields(st: &DeriveInput) -> syn::Result<&StructFields> {
156    if let syn::Data::Struct(DataStruct {
157        fields:
158            syn::Fields::Named(FieldsNamed {
159                named: ref struct_fields,
160                ..
161            }),
162        ..
163    }) = &st.data
164    {
165        Ok(struct_fields)
166    } else {
167        Err(syn::Error::new_spanned(
168            st,
169            "IOExcel can only use to struct not enum or other !",
170        ))
171    }
172}
173
174fn get_inner_type_by_target(field: &Field, target: &str) -> Option<syn::Type> {
175    if let Field {
176        ty:
177            syn::Type::Path(syn::TypePath {
178                path: syn::Path { ref segments, .. },
179                ..
180            }),
181        ..
182    } = field
183    {
184        if let Some(syn::PathSegment { ref ident, .. }) = segments.first() {
185            let ident_letial = ident.to_string();
186            if ident_letial == target {
187                if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
188                    ref args,
189                    ..
190                }) = segments.first().unwrap().arguments
191                {
192                    if let Some(syn::GenericArgument::Type(ty)) = args.first() {
193                        return Some(ty.clone());
194                    }
195                }
196            }
197        }
198    }
199    None
200}
201
202struct IOExcelField {
203    ident: Ident,
204    ty: syn::Type,
205    column_name: Option<String>,
206    is_option: bool,
207}
208
209fn get_io_excel_field(field: &Field) -> syn::Result<IOExcelField> {
210    let ident = &field.ident;
211    let ident_letial = &ident.as_ref().unwrap().to_string();
212    let excel_ident = Ident::new(ident_letial, ident.span());
213    let type_ = field.ty.clone();
214    let mut column_name = None;
215    let is_option = get_inner_type_by_target(field, "Option").is_some();
216    for attr in &field.attrs {
217        if let Ok(syn::Meta::List(list)) = attr.parse_meta() {
218            if list.path.is_ident("column") {
219                for arg in list.nested {
220                    if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = arg {
221                        if name_value.path.is_ident("name") {
222                            if let syn::Lit::Str(lit_str) = name_value.lit {
223                                column_name = Some(lit_str.value());
224                                return Ok(IOExcelField {
225                                    ident: excel_ident,
226                                    ty: type_,
227                                    column_name,
228                                    is_option,
229                                });
230                            }
231                        }
232                    }
233                }
234            }
235        }
236    }
237    Ok(IOExcelField {
238        ident: excel_ident,
239        ty: type_,
240        column_name,
241        is_option,
242    })
243}