gluesql-macros 0.20.0

Derive macro for gluesql row conversion
Documentation
use {
    crate::{parse_glue_rename, resolve_gluesql_crate},
    quote::quote,
    syn::{Data, DeriveInput, Fields, spanned::Spanned},
};

pub(crate) fn expand_to_glue_row(
    input: DeriveInput,
) -> Result<proc_macro2::TokenStream, syn::Error> {
    let input_span = input.span();

    let gluesql_crate_path = resolve_gluesql_crate()?;
    let gluesql_crate = quote! { #gluesql_crate_path };

    let ident = input.ident.clone();
    let Data::Struct(data) = input.data else {
        return Err(syn::Error::new(
            input_span,
            "ToGlueRow can only be derived for structs",
        ));
    };

    let fields = match data.fields {
        Fields::Named(f) => f.named,
        _ => {
            return Err(syn::Error::new(
                input_span,
                "ToGlueRow supports only named fields",
            ));
        }
    };

    let mut seen_columns: Vec<String> = Vec::new();
    let mut column_names = Vec::new();
    let mut field_literals = Vec::new();

    for field in &fields {
        let field_ident = field.ident.clone().expect("named field");
        let field_name_literal = field_ident.to_string();

        let mut rename: Option<String> = None;
        for attr in &field.attrs {
            if let Some(res) = parse_glue_rename(attr) {
                match res {
                    Ok(Some(name)) => rename = Some(name),
                    Ok(None) => {}
                    Err(e) => return Err(e),
                }
            }
        }
        let column_name = rename.unwrap_or_else(|| field_name_literal.clone());
        if seen_columns.contains(&column_name) {
            return Err(syn::Error::new(
                field.span(),
                format!("duplicate column name `{column_name}`"),
            ));
        }
        seen_columns.push(column_name.clone());
        column_names.push(quote! { #column_name });

        field_literals.push(quote! {
            #gluesql_crate::translate::IntoParamLiteral::into_param_literal(
                ::core::clone::Clone::clone(&self.#field_ident)
            )
        });
    }

    let columns_len = column_names.len();
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let expanded = quote! {
        impl #impl_generics #gluesql_crate::row_conversion::ToGlueRow for #ident #ty_generics #where_clause {
            fn glue_columns() -> &'static [&'static str] {
                static COLUMNS: [&str; #columns_len] = [ #(#column_names),* ];
                &COLUMNS
            }

            fn to_glue_row(&self) -> ::std::vec::Vec<#gluesql_crate::translate::ParamLiteral> {
                ::std::vec![ #(#field_literals),* ]
            }
        }
    };

    Ok(expanded)
}

#[cfg(test)]
mod tests {
    use super::expand_to_glue_row;
    use syn::parse_quote;

    #[test]
    fn non_struct_input_returns_error() {
        let di: syn::DeriveInput = parse_quote! {
            enum E { A }
        };
        let err = expand_to_glue_row(di).unwrap_err();
        assert!(
            err.to_string()
                .contains("ToGlueRow can only be derived for structs")
        );
    }

    #[test]
    fn non_named_fields_struct_returns_error() {
        let di: syn::DeriveInput = parse_quote! {
            struct T(i32);
        };
        let err = expand_to_glue_row(di).unwrap_err();
        assert!(
            err.to_string()
                .contains("ToGlueRow supports only named fields")
        );
    }

    #[test]
    fn glue_rename_ok_some_and_ok_none() {
        let di: syn::DeriveInput = parse_quote! {
            struct S {
                #[glue(rename = "col")] a: i64,
                #[glue(other = "x")] b: String,
            }
        };
        let _ = expand_to_glue_row(di).expect("expand ok");
    }

    #[test]
    fn duplicate_column_name_returns_error() {
        let di: syn::DeriveInput = parse_quote! {
            struct S {
                value: i64,
                #[glue(rename = "value")]
                previous_value: i64,
            }
        };
        let err = expand_to_glue_row(di).unwrap_err();
        assert!(err.to_string().contains("duplicate column name `value`"));
    }

    #[test]
    fn glue_rename_wrong_literal_type() {
        let di: syn::DeriveInput = parse_quote! {
            struct S { #[glue(rename = 123)] a: i64 }
        };
        let err = expand_to_glue_row(di).unwrap_err();
        assert!(
            err.to_string()
                .contains("expected string literal for rename")
        );
    }

    #[test]
    fn generics_and_lifetimes_preserved() {
        let di: syn::DeriveInput = parse_quote! {
            struct Record<'a, T>
            where
                T: Clone + crate::translate::IntoParamLiteral,
            {
                name: &'a str,
                value: T,
            }
        };
        let ts = expand_to_glue_row(di).expect("expand ok").to_string();
        assert!(ts.contains("impl < 'a , T >"));
        assert!(ts.contains("ToGlueRow for Record < 'a , T >"));
        assert!(ts.contains("where T : Clone + crate :: translate :: IntoParamLiteral"));
    }
}