Derive macro aggregating several derive macros?

Is it possible to write a derive macro aggregating several derive macros?
As I understand not, but maybe there is a way?

What exactly do you mean by "aggregating"?


pub fn diesel_macros(
    args: &AttributeArgs,
    item: &ItemEnum,
) -> syn::Result<TokenStream> {
    let name = &item.ident;
    let struct_name = format!("Database{name}");
    let tokens = item.to_token_stream();
    Ok(quote! {
        #[derive(diesel::AsExpression, diesel::FromSqlRow)]
        #[sql_type = #struct_name]
        #tokens
    })
}

Calling several macros internally with preferred parameters.
It's function-like. But I would prefer derive-like.

It is allowed to put multiple trait names in a single #[derive(…)] clause, if that's what is being asked.

Not really.
Sample code calling several macros internally with preferred parameters.
It's function-like. But I would prefer derive-like.

The code prepend code with

        #[derive(diesel::AsExpression, diesel::FromSqlRow)]
        #[sql_type = #struct_name]

A derive macro can't modify its input (the struct/enum/union declaration it is attached to), it can only add further items below the input. So if you want to modify the declaration, then you can't do that with a derive macro. But if you prefer attribute syntax, then you could write a custom attribute macro instead of a function-like (or derive) macro.

1 Like

Writing attribute macro is a good idea! Thanks.

Indeed the main answer is no: a derive macro cannot add derives to the annotated item, since that would constitute a mutation. An attribute macro, on the other hand, easily can.

FWIW, there is a way, albeit one which breaks unless your own derive is marked as having the derive helper attributes of the derives you wish to emit, by virtue of reëmitting the original item, with the derives on it, followed by a nuking attribute macro so that the duplicate item is not emitted:

#[doc(hidden)] /** Not part of the public API */
#[proc_macro_attribute] pub
fn annihilate (
    _: TokenStream,
    _: TokenStream,
) -> TokenStream
{
    <_>::default()
}

and:

pub fn diesel_macros(
    args: &AttributeArgs,
    item: &ItemEnum,
) -> syn::Result<TokenStream> {
    let name = &item.ident;
    let struct_name = format!("Database{name}");
    let tokens = item.to_token_stream();
    Ok(quote! {
        #[derive(diesel::AsExpression, diesel::FromSqlRow)]
        #[sql_type = #struct_name]
+       #[::your_crate_name::annihilate]
        #tokens
    })
}
  • (plus tweaking the derive helper attributes so that they include sql_type).

  • Aside: calling .to_token_stream() before feeding something to quote! is needlessly redundant, you can directly do quote!( #[…] #item ) :slightly_smiling_face:


Needless to say using an attribute macro directly is the proper approach, and this other hack technique should be used sparingly, only when needed.

2 Likes

Wow! I understood that function-like way is a proper one. Are you saying that it's possible to use derive macro instead of function-like macro? If so why diesel_macros expects 1 arguments instead of 2?

#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}

@Yandros could you please elaborate a bit more the hack?

It's possible to use attribute macro, not derive macro. That is, you'll have

#[derive_diesel]
struct Foo { ... }

instead of

#[derive(SomeTrait)]
struct Foo { ... }

where derive_diesel will emit all necessary derive attributes.

1 Like
#[doc(hidden)] /** Not part of the public API */
#[proc_macro_attribute] pub
fn __annihilate (
    _: TokenStream,
    _: TokenStream,
) -> TokenStream
{
    <_>::default()
}

#[proc_macro_derive(
    Petrol,
    // attributes(sql_type), /* in case you wanted to allow extra `#[sql_type]` annotations under your derive */
)]
pub fn petrol(item: TokenStream) -> TokenStream {
    let mut item: ItemStruct = parse_macro_input!(item);
    let name = &item.ident;
    let struct_name_str = format!("Database{name}");
    // ensure the generated item definition does not end up actually emitted.
    item.attrs.push(parse_quote!(
        #[::your_crate::__annihilate]
    ));
    // "re"-emit the item with the desired derives
    quote!(
        #[::core::prelude::v1::derive(
            ::diesel::AsExpression, // <- ideally these would be frontend-crate-qualified
            ::diesel::FromSqlRow,
        )]
        #[sql_type = #struct_name_str]
        #item
    ).into()
}

That way #[derive(Petrol)] should "stand for" #[derive(AsExpression, FromSqlRow)]

2 Likes