excelx-derive 0.4.1

Derive macro for the excelx ExcelRow trait.
Documentation
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
    Data, DeriveInput, Expr, ExprLit, Fields, GenericArgument, Lit, PathArguments, Type,
    parse_macro_input,
};

#[proc_macro_derive(ExcelRow, attributes(excel))]
pub fn derive_excel_row(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    expand_excel_row(&input)
        .unwrap_or_else(syn::Error::into_compile_error)
        .into()
}

fn expand_excel_row(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let ident = &input.ident;
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => {
                return Err(syn::Error::new_spanned(
                    input,
                    "ExcelRow can only be derived for structs with named fields",
                ));
            }
        },
        _ => {
            return Err(syn::Error::new_spanned(
                input,
                "ExcelRow can only be derived for structs",
            ));
        }
    };

    let mut column_defs = Vec::with_capacity(fields.len());
    let mut to_row_values = Vec::with_capacity(fields.len());
    let mut from_row_fields = Vec::with_capacity(fields.len());

    for field in fields {
        let field_ident = field
            .ident
            .as_ref()
            .ok_or_else(|| syn::Error::new_spanned(field, "field must be named"))?;
        let field_name = field_ident.to_string();
        let attrs = ExcelAttrs::parse(field)?;
        let header = attrs
            .header
            .ok_or_else(|| syn::Error::new_spanned(field, "missing #[excel(header = \"...\")]"))?;
        let order = attrs
            .order
            .ok_or_else(|| syn::Error::new_spanned(field, "missing #[excel(order = ...)]"))?;

        let default_tokens = attrs
            .default
            .as_ref()
            .map(|value| quote! { Some(#value) })
            .unwrap_or_else(|| quote! { None });

        column_defs.push(quote! {
            ::excelx::ColumnDef {
                field: #field_name,
                header: #header,
                order: #order,
                default: #default_tokens,
            }
        });

        let conversion = FieldConversion::for_type(&field.ty)?;
        to_row_values.push(conversion.to_cell_value(field_ident)?);
        from_row_fields.push(conversion.build_field_initializer(field_ident, &field_name)?);
    }

    Ok(quote! {
        impl ::excelx::ExcelRow for #ident {
            fn columns() -> ::std::vec::Vec<::excelx::ColumnDef> {
                ::std::vec![#(#column_defs),*]
            }

            fn to_row(&self) -> ::std::vec::Vec<::excelx::CellValue> {
                ::std::vec![#(#to_row_values),*]
            }

            fn from_row(row: &::excelx::RowView) -> ::std::result::Result<Self, ::excelx::ExcelError> {
                ::std::result::Result::Ok(Self {
                    #(#from_row_fields),*
                })
            }
        }
    })
}

#[derive(Default)]
struct ExcelAttrs {
    header: Option<String>,
    order: Option<usize>,
    default: Option<String>,
}

impl ExcelAttrs {
    fn parse(field: &syn::Field) -> syn::Result<Self> {
        let mut attrs = Self::default();

        for attr in &field.attrs {
            if !attr.path().is_ident("excel") {
                continue;
            }

            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("header") {
                    let value = meta.value()?;
                    attrs.header = Some(value.parse::<syn::LitStr>()?.value());
                    Ok(())
                } else if meta.path.is_ident("order") {
                    let value = meta.value()?;
                    attrs.order = Some(parse_usize_lit(value.parse::<Expr>()?)?);
                    Ok(())
                } else if meta.path.is_ident("default") {
                    let value = meta.value()?;
                    attrs.default = Some(value.parse::<syn::LitStr>()?.value());
                    Ok(())
                } else {
                    Err(meta.error("unsupported excel attribute"))
                }
            })?;
        }

        Ok(attrs)
    }
}

fn parse_usize_lit(expr: Expr) -> syn::Result<usize> {
    match expr {
        Expr::Lit(ExprLit {
            lit: Lit::Int(value),
            ..
        }) => value.base10_parse(),
        other => Err(syn::Error::new_spanned(
            other,
            "order must be an unsigned integer literal",
        )),
    }
}

enum FieldConversion<'a> {
    String,
    Integer(&'a Type),
    Float(&'a Type),
    Bool,
    Option(Box<FieldConversion<'a>>),
}

impl<'a> FieldConversion<'a> {
    fn for_type(ty: &'a Type) -> syn::Result<Self> {
        if let Some(inner) = option_inner_type(ty) {
            if option_inner_type(inner).is_some() {
                return Err(syn::Error::new_spanned(
                    ty,
                    "nested Option fields are not supported",
                ));
            }

            return Ok(Self::Option(Box::new(Self::for_type(inner)?)));
        }

        if type_is(ty, "String") {
            return Ok(Self::String);
        }

        if type_is(ty, "bool") {
            return Ok(Self::Bool);
        }

        if is_supported_integer(ty) {
            return Ok(Self::Integer(ty));
        }

        if is_supported_float(ty) {
            return Ok(Self::Float(ty));
        }

        Err(syn::Error::new_spanned(
            ty,
            "unsupported ExcelRow field type",
        ))
    }

    fn to_cell_value(&self, field_ident: &syn::Ident) -> syn::Result<proc_macro2::TokenStream> {
        match self {
            Self::String => Ok(quote! { ::excelx::CellValue::String(self.#field_ident.clone()) }),
            Self::Integer(_) => Ok(quote! { ::excelx::CellValue::Int(self.#field_ident.into()) }),
            Self::Float(ty) if type_is(ty, "f32") => Ok(
                quote! { ::excelx::CellValue::Float(::std::convert::Into::<f64>::into(self.#field_ident)) },
            ),
            Self::Float(_) => Ok(quote! { ::excelx::CellValue::Float(self.#field_ident) }),
            Self::Bool => Ok(quote! { ::excelx::CellValue::Bool(self.#field_ident) }),
            Self::Option(inner) => {
                let value_ident = format_ident!("value");
                let inner_tokens = inner.to_cell_value_for_value(&value_ident)?;
                Ok(quote! {
                    match &self.#field_ident {
                        ::std::option::Option::Some(#value_ident) => #inner_tokens,
                        ::std::option::Option::None => ::excelx::CellValue::Empty,
                    }
                })
            }
        }
    }

    fn to_cell_value_for_value(
        &self,
        value_ident: &syn::Ident,
    ) -> syn::Result<proc_macro2::TokenStream> {
        match self {
            Self::String => Ok(quote! { ::excelx::CellValue::String(#value_ident.clone()) }),
            Self::Integer(_) => Ok(quote! { ::excelx::CellValue::Int((*#value_ident).into()) }),
            Self::Float(ty) if type_is(ty, "f32") => Ok(
                quote! { ::excelx::CellValue::Float(::std::convert::Into::<f64>::into(*#value_ident)) },
            ),
            Self::Float(_) => Ok(quote! { ::excelx::CellValue::Float(*#value_ident) }),
            Self::Bool => Ok(quote! { ::excelx::CellValue::Bool(*#value_ident) }),
            Self::Option(_) => Err(syn::Error::new_spanned(
                value_ident,
                "nested Option fields are not supported",
            )),
        }
    }

    fn build_field_initializer(
        &self,
        field_ident: &syn::Ident,
        field_name: &str,
    ) -> syn::Result<proc_macro2::TokenStream> {
        let value_expr = self.required_accessor_expr(field_name)?;
        Ok(quote! { #field_ident: #value_expr })
    }

    fn required_accessor_expr(&self, field_name: &str) -> syn::Result<proc_macro2::TokenStream> {
        match self {
            Self::String => Ok(quote! { row.required_string(#field_name)? }),
            Self::Integer(ty) if type_is(ty, "i64") => {
                Ok(quote! { row.required_i64(#field_name)? })
            }
            Self::Integer(ty) => Ok(quote! {
                row.required_i64(#field_name)?.try_into().map_err(|_| {
                    ::excelx::ExcelError::InvalidCellType {
                        row: row.row_number(),
                        column: row.header_for_field(#field_name),
                        expected: ::std::stringify!(#ty).to_owned(),
                        found: "integer out of range".to_owned(),
                    }
                })?
            }),
            Self::Float(ty) if type_is(ty, "f32") => {
                Ok(quote! { row.required_f64(#field_name)? as f32 })
            }
            Self::Float(_) => Ok(quote! { row.required_f64(#field_name)? }),
            Self::Bool => Ok(quote! { row.required_bool(#field_name)? }),
            Self::Option(inner) => inner.optional_accessor_expr(field_name),
        }
    }

    fn optional_accessor_expr(&self, field_name: &str) -> syn::Result<proc_macro2::TokenStream> {
        match self {
            Self::String => Ok(quote! { row.optional_string(#field_name)? }),
            Self::Integer(ty) if type_is(ty, "i64") => {
                Ok(quote! { row.optional_i64(#field_name)? })
            }
            Self::Integer(ty) => Ok(quote! {
                match row.optional_i64(#field_name)? {
                    ::std::option::Option::Some(value) => {
                        ::std::option::Option::Some(value.try_into().map_err(|_| {
                            ::excelx::ExcelError::InvalidCellType {
                                row: row.row_number(),
                                column: row.header_for_field(#field_name),
                                expected: ::std::stringify!(#ty).to_owned(),
                                found: "integer out of range".to_owned(),
                            }
                        })?)
                    }
                    ::std::option::Option::None => ::std::option::Option::None,
                }
            }),
            Self::Float(ty) if type_is(ty, "f32") => {
                Ok(quote! { row.optional_f64(#field_name)?.map(|value| value as f32) })
            }
            Self::Float(_) => Ok(quote! { row.optional_f64(#field_name)? }),
            Self::Bool => Ok(quote! { row.optional_bool(#field_name)? }),
            Self::Option(_) => Err(syn::Error::new_spanned(
                field_name,
                "nested Option fields are not supported",
            )),
        }
    }
}

fn option_inner_type(ty: &Type) -> Option<&Type> {
    let Type::Path(type_path) = ty else {
        return None;
    };

    let segment = type_path.path.segments.last()?;
    if segment.ident != "Option" {
        return None;
    }

    let PathArguments::AngleBracketed(args) = &segment.arguments else {
        return None;
    };

    match args.args.first()? {
        GenericArgument::Type(inner) => Some(inner),
        _ => None,
    }
}

fn type_is(ty: &Type, expected: &str) -> bool {
    let Type::Path(type_path) = ty else {
        return false;
    };

    type_path
        .path
        .segments
        .last()
        .is_some_and(|segment| segment.ident == expected)
}

fn is_supported_integer(ty: &Type) -> bool {
    ["i8", "i16", "i32", "i64", "u8", "u16", "u32"]
        .iter()
        .any(|expected| type_is(ty, expected))
}

fn is_supported_float(ty: &Type) -> bool {
    ["f32", "f64"].iter().any(|expected| type_is(ty, expected))
}