Skip to main content

excelx_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4    Data, DeriveInput, Expr, ExprLit, Fields, GenericArgument, Lit, PathArguments, Type,
5    parse_macro_input,
6};
7
8#[proc_macro_derive(ExcelRow, attributes(excel))]
9pub fn derive_excel_row(input: TokenStream) -> TokenStream {
10    let input = parse_macro_input!(input as DeriveInput);
11
12    expand_excel_row(&input)
13        .unwrap_or_else(syn::Error::into_compile_error)
14        .into()
15}
16
17fn expand_excel_row(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
18    let ident = &input.ident;
19    let fields = match &input.data {
20        Data::Struct(data) => match &data.fields {
21            Fields::Named(fields) => &fields.named,
22            _ => {
23                return Err(syn::Error::new_spanned(
24                    input,
25                    "ExcelRow can only be derived for structs with named fields",
26                ));
27            }
28        },
29        _ => {
30            return Err(syn::Error::new_spanned(
31                input,
32                "ExcelRow can only be derived for structs",
33            ));
34        }
35    };
36
37    let mut column_defs = Vec::with_capacity(fields.len());
38    let mut to_row_values = Vec::with_capacity(fields.len());
39    let mut from_row_fields = Vec::with_capacity(fields.len());
40
41    for field in fields {
42        let field_ident = field
43            .ident
44            .as_ref()
45            .ok_or_else(|| syn::Error::new_spanned(field, "field must be named"))?;
46        let field_name = field_ident.to_string();
47        let attrs = ExcelAttrs::parse(field)?;
48        let header = attrs
49            .header
50            .ok_or_else(|| syn::Error::new_spanned(field, "missing #[excel(header = \"...\")]"))?;
51        let order = attrs
52            .order
53            .ok_or_else(|| syn::Error::new_spanned(field, "missing #[excel(order = ...)]"))?;
54
55        let default_tokens = attrs
56            .default
57            .as_ref()
58            .map(|value| quote! { Some(#value) })
59            .unwrap_or_else(|| quote! { None });
60
61        column_defs.push(quote! {
62            ::excelx::ColumnDef {
63                field: #field_name,
64                header: #header,
65                order: #order,
66                default: #default_tokens,
67            }
68        });
69
70        let conversion = FieldConversion::for_type(&field.ty)?;
71        to_row_values.push(conversion.to_cell_value(field_ident)?);
72        from_row_fields.push(conversion.build_field_initializer(field_ident, &field_name)?);
73    }
74
75    Ok(quote! {
76        impl ::excelx::ExcelRow for #ident {
77            fn columns() -> ::std::vec::Vec<::excelx::ColumnDef> {
78                ::std::vec![#(#column_defs),*]
79            }
80
81            fn to_row(&self) -> ::std::vec::Vec<::excelx::CellValue> {
82                ::std::vec![#(#to_row_values),*]
83            }
84
85            fn from_row(row: &::excelx::RowView) -> ::std::result::Result<Self, ::excelx::ExcelError> {
86                ::std::result::Result::Ok(Self {
87                    #(#from_row_fields),*
88                })
89            }
90        }
91    })
92}
93
94#[derive(Default)]
95struct ExcelAttrs {
96    header: Option<String>,
97    order: Option<usize>,
98    default: Option<String>,
99}
100
101impl ExcelAttrs {
102    fn parse(field: &syn::Field) -> syn::Result<Self> {
103        let mut attrs = Self::default();
104
105        for attr in &field.attrs {
106            if !attr.path().is_ident("excel") {
107                continue;
108            }
109
110            attr.parse_nested_meta(|meta| {
111                if meta.path.is_ident("header") {
112                    let value = meta.value()?;
113                    attrs.header = Some(value.parse::<syn::LitStr>()?.value());
114                    Ok(())
115                } else if meta.path.is_ident("order") {
116                    let value = meta.value()?;
117                    attrs.order = Some(parse_usize_lit(value.parse::<Expr>()?)?);
118                    Ok(())
119                } else if meta.path.is_ident("default") {
120                    let value = meta.value()?;
121                    attrs.default = Some(value.parse::<syn::LitStr>()?.value());
122                    Ok(())
123                } else {
124                    Err(meta.error("unsupported excel attribute"))
125                }
126            })?;
127        }
128
129        Ok(attrs)
130    }
131}
132
133fn parse_usize_lit(expr: Expr) -> syn::Result<usize> {
134    match expr {
135        Expr::Lit(ExprLit {
136            lit: Lit::Int(value),
137            ..
138        }) => value.base10_parse(),
139        other => Err(syn::Error::new_spanned(
140            other,
141            "order must be an unsigned integer literal",
142        )),
143    }
144}
145
146enum FieldConversion<'a> {
147    String,
148    Integer(&'a Type),
149    Float(&'a Type),
150    Bool,
151    Option(Box<FieldConversion<'a>>),
152}
153
154impl<'a> FieldConversion<'a> {
155    fn for_type(ty: &'a Type) -> syn::Result<Self> {
156        if let Some(inner) = option_inner_type(ty) {
157            if option_inner_type(inner).is_some() {
158                return Err(syn::Error::new_spanned(
159                    ty,
160                    "nested Option fields are not supported",
161                ));
162            }
163
164            return Ok(Self::Option(Box::new(Self::for_type(inner)?)));
165        }
166
167        if type_is(ty, "String") {
168            return Ok(Self::String);
169        }
170
171        if type_is(ty, "bool") {
172            return Ok(Self::Bool);
173        }
174
175        if is_supported_integer(ty) {
176            return Ok(Self::Integer(ty));
177        }
178
179        if is_supported_float(ty) {
180            return Ok(Self::Float(ty));
181        }
182
183        Err(syn::Error::new_spanned(
184            ty,
185            "unsupported ExcelRow field type",
186        ))
187    }
188
189    fn to_cell_value(&self, field_ident: &syn::Ident) -> syn::Result<proc_macro2::TokenStream> {
190        match self {
191            Self::String => Ok(quote! { ::excelx::CellValue::String(self.#field_ident.clone()) }),
192            Self::Integer(_) => Ok(quote! { ::excelx::CellValue::Int(self.#field_ident.into()) }),
193            Self::Float(ty) if type_is(ty, "f32") => Ok(
194                quote! { ::excelx::CellValue::Float(::std::convert::Into::<f64>::into(self.#field_ident)) },
195            ),
196            Self::Float(_) => Ok(quote! { ::excelx::CellValue::Float(self.#field_ident) }),
197            Self::Bool => Ok(quote! { ::excelx::CellValue::Bool(self.#field_ident) }),
198            Self::Option(inner) => {
199                let value_ident = format_ident!("value");
200                let inner_tokens = inner.to_cell_value_for_value(&value_ident)?;
201                Ok(quote! {
202                    match &self.#field_ident {
203                        ::std::option::Option::Some(#value_ident) => #inner_tokens,
204                        ::std::option::Option::None => ::excelx::CellValue::Empty,
205                    }
206                })
207            }
208        }
209    }
210
211    fn to_cell_value_for_value(
212        &self,
213        value_ident: &syn::Ident,
214    ) -> syn::Result<proc_macro2::TokenStream> {
215        match self {
216            Self::String => Ok(quote! { ::excelx::CellValue::String(#value_ident.clone()) }),
217            Self::Integer(_) => Ok(quote! { ::excelx::CellValue::Int((*#value_ident).into()) }),
218            Self::Float(ty) if type_is(ty, "f32") => Ok(
219                quote! { ::excelx::CellValue::Float(::std::convert::Into::<f64>::into(*#value_ident)) },
220            ),
221            Self::Float(_) => Ok(quote! { ::excelx::CellValue::Float(*#value_ident) }),
222            Self::Bool => Ok(quote! { ::excelx::CellValue::Bool(*#value_ident) }),
223            Self::Option(_) => Err(syn::Error::new_spanned(
224                value_ident,
225                "nested Option fields are not supported",
226            )),
227        }
228    }
229
230    fn build_field_initializer(
231        &self,
232        field_ident: &syn::Ident,
233        field_name: &str,
234    ) -> syn::Result<proc_macro2::TokenStream> {
235        let value_expr = self.required_accessor_expr(field_name)?;
236        Ok(quote! { #field_ident: #value_expr })
237    }
238
239    fn required_accessor_expr(&self, field_name: &str) -> syn::Result<proc_macro2::TokenStream> {
240        match self {
241            Self::String => Ok(quote! { row.required_string(#field_name)? }),
242            Self::Integer(ty) if type_is(ty, "i64") => {
243                Ok(quote! { row.required_i64(#field_name)? })
244            }
245            Self::Integer(ty) => Ok(quote! {
246                row.required_i64(#field_name)?.try_into().map_err(|_| {
247                    ::excelx::ExcelError::InvalidCellType {
248                        row: row.row_number(),
249                        column: row.header_for_field(#field_name),
250                        expected: ::std::stringify!(#ty).to_owned(),
251                        found: "integer out of range".to_owned(),
252                    }
253                })?
254            }),
255            Self::Float(ty) if type_is(ty, "f32") => {
256                Ok(quote! { row.required_f64(#field_name)? as f32 })
257            }
258            Self::Float(_) => Ok(quote! { row.required_f64(#field_name)? }),
259            Self::Bool => Ok(quote! { row.required_bool(#field_name)? }),
260            Self::Option(inner) => inner.optional_accessor_expr(field_name),
261        }
262    }
263
264    fn optional_accessor_expr(&self, field_name: &str) -> syn::Result<proc_macro2::TokenStream> {
265        match self {
266            Self::String => Ok(quote! { row.optional_string(#field_name)? }),
267            Self::Integer(ty) if type_is(ty, "i64") => {
268                Ok(quote! { row.optional_i64(#field_name)? })
269            }
270            Self::Integer(ty) => Ok(quote! {
271                match row.optional_i64(#field_name)? {
272                    ::std::option::Option::Some(value) => {
273                        ::std::option::Option::Some(value.try_into().map_err(|_| {
274                            ::excelx::ExcelError::InvalidCellType {
275                                row: row.row_number(),
276                                column: row.header_for_field(#field_name),
277                                expected: ::std::stringify!(#ty).to_owned(),
278                                found: "integer out of range".to_owned(),
279                            }
280                        })?)
281                    }
282                    ::std::option::Option::None => ::std::option::Option::None,
283                }
284            }),
285            Self::Float(ty) if type_is(ty, "f32") => {
286                Ok(quote! { row.optional_f64(#field_name)?.map(|value| value as f32) })
287            }
288            Self::Float(_) => Ok(quote! { row.optional_f64(#field_name)? }),
289            Self::Bool => Ok(quote! { row.optional_bool(#field_name)? }),
290            Self::Option(_) => Err(syn::Error::new_spanned(
291                field_name,
292                "nested Option fields are not supported",
293            )),
294        }
295    }
296}
297
298fn option_inner_type(ty: &Type) -> Option<&Type> {
299    let Type::Path(type_path) = ty else {
300        return None;
301    };
302
303    let segment = type_path.path.segments.last()?;
304    if segment.ident != "Option" {
305        return None;
306    }
307
308    let PathArguments::AngleBracketed(args) = &segment.arguments else {
309        return None;
310    };
311
312    match args.args.first()? {
313        GenericArgument::Type(inner) => Some(inner),
314        _ => None,
315    }
316}
317
318fn type_is(ty: &Type, expected: &str) -> bool {
319    let Type::Path(type_path) = ty else {
320        return false;
321    };
322
323    type_path
324        .path
325        .segments
326        .last()
327        .is_some_and(|segment| segment.ident == expected)
328}
329
330fn is_supported_integer(ty: &Type) -> bool {
331    ["i8", "i16", "i32", "i64", "u8", "u16", "u32"]
332        .iter()
333        .any(|expected| type_is(ty, expected))
334}
335
336fn is_supported_float(ty: &Type) -> bool {
337    ["f32", "f64"].iter().any(|expected| type_is(ty, expected))
338}