Derive macro to annotate struct variables

We have a limited amount of time to dedicate to helping people in this forum, so we do need that the person asking for help do try to do stuff on their own from the help they've received.

For instance, derive macros is just a tool for the macro to write Rust code in your stead. So while you may very legitimately ask about that part as this thread does, the whole thing assumes that you are already able to manually write the very Rust code that the macro code ought to generate for you. This is a paramount thing for macros: they help automate writing annoying / boilerplate-y code, but in order to do that, one needs to know of that boilerplate-y code to begin with.

Since your problem started very abstract, with a random 4 appearing in the example, I assumed that it was some special constant and hence I suggested the:

design.

But now, your usage looks like this:

So now we know that 4 wasn't a hard-coded value, but the runtime value of the auth_errors field.

  • The method will now need to depend on an instance:

    - fn prometheus_annotations() …
    + fn prometheus_annotations(&self) …
    
  • The method cannot return a compile-time string (cannot be a &'static str) anymore: either it needs to take a &mut fmt::Formatter<'_> or a &mut {dyn,impl} fmt::Write "output stream", or, to keep things simple, since perfomance does not seem important for this use case, to return a runtime-generated string, that is, a String:

    - fn prometheus_annotations(…) -> &'static str
    + fn prometheus_annotations(…) -> String
    

These kind of things / remarks should have come from you, since you know your problem space (we, on the other side, can only guess what you are after, and from little information our guesses are just "bets" that can miss the target, like mines just have).

It may be that you are not comfortable enough even with these other Rust questions such as the difference between a non-method / static associated function and a method, or between a &'static str and a String —which is a fine/understandable thing! we all have to start with Rust at some point :slightly_smiling_face:— but in that case trying to simultaneously tackle writing a derive macro might have been a bit too much to chew, since that suddenly mixes the macro's logic itself (the meta-programming code), with the code it generates, and so things have to be very clear in one's head not to mix up the two.


So, let's go back to forgetting about the proc-macro, and just try to implement the PrometheusAnnotations trait for your Stats data structure:

pub struct Stats {
    /// auth label
    #[metric_type("counter")]
    auth_errors: u64,
}

/// Manually / hand-rolled implementation.
impl PrometheusAnnotations for Stats {
    fn prometheus_annotations (
        self: &'_ Stats,
    ) -> String
    {
        use ::core::fmt::Write;
        let mut ret = String::new();

        // the docstring
        writeln!(ret, "HELP {docstring}", docstring = "auth label").unwrap();

        // the metryc type
        writeln!(ret, "TYPE {type_}", type_ = "counter").unwrap();

        // the runtime value
        writeln!(ret,
            "{field_name} {value:?}\n",
            field_name = "auth_errors",
            value = &self.auth_errors,
        ).unwrap();

        ret
    }
}

Now, you should already try your code and make sure it works properly from there. If there are errors when doing so, I suspect they will involve small details / typos that either you'll be able to figure out, or which you can ask about, but maybe in another thread since it won't involve derive macros logic.


Once the manually-written implementation works correctly

Only then is it time to write the macro / meta-programming-code in charge of generating it.

As you can see, we'll need to emit a bunch of writeln!s for the function's body, where the function body itself will be a "buffer" of sorts, on its own, in the meta-programming world.

So we'll end up with:

let docstring: String = …;
function_body.extend(quote!(
    // the docstring
    writeln!(ret, "HELP {docstring}", docstring = #docstring).unwrap();
));

kind of snippets. Notice how we are appending stuff to our function_body variable, and what we are appending is Rust code (quote!(…) contents) with a #docstring meta-programming-world-runtime-variable (and thus compile-time string for the emitted code) that we are thus interpolating as a string literal. If this sounds confusing, then we are back to "one needs to have the notions of compile-time and runtime, as well as meta-programming vs. emitted code, very clear in the head in order to properly distinguish these notions.

And now we can rewrite my for field in &input.fields { meta-programming loop:

    // this is the function's body's prelude (cf. above in my post)
    let mut function_body = quote!();
    // Now time to add the docstring, metric type, and field value to that `ret`
    for field in &input.fields {
        // 1. Handle the docstrings
        let attrs = &field.attrs;
        let doc_strings =
            attrs.iter().filter_map(|attr| bool::then(
                attr.path.is_ident("doc"),
                || parse2::<DocString>(attr.tokens.clone()).ok(),
            ).flatten())
        ;
        doc_strings.for_each(|DocString(doc_string)| {
            function_body.extend(quote_spanned!(Span::mixed_site()=>
                ::std::writeln!(ret, "HELP {}", #doc_string)
                    .unwrap()
                ;
            ));
        });

        // 2. Handle the metric_type
        let mut metric_types = attrs.iter().filter(|attr| attr.path.is_ident("metric_type"));
        match (metric_types.next(), metric_types.next()) {
            | (Some(attr), None) => {
                let metric_type: String = attr.parse_args::<LitStr>()?.value();
                function_body.extend(quote_spanned!(Span::mixed_site()=>
                    ::std::writeln!(ret, "TYPE {}", #metric_type)
                        .unwrap()
                    ;
                ));
            },
            | (None, _) => {
                // No `metric_type` provided: error or ignore?
                …
            },
            | (Some(_), Some(duplicate)) => return Err(Error::new_spanned(
                // code to highlight / blame
                duplicate,
                // error message
                "Duplicate `metric_type` attribute",
            )),
        };

        // 3. Finally, the runtime value
        if let Some(ref field_name) = field.ident {
            function_body.extend(quote_spanned!(Span::mixed_site()=>
                ::std::writeln!(ret,
                    "{field_name} {value:?}\n",
                    field_name = ::core::stringify!(#field_name),
                    value = &self.#field_name
                ).unwrap();
            ));
        } else {
            return Err(Error::new(Span::call_site(), "expected a braced struct"));
        }
    }

this has given us a function_body: TokenStream2 variable (TokenStream2 is one of the types representing meta-programming-runtime Rust code, which represent the contents of the method body (the stuff inside the braces / the braced block), but for the function's prelude (declaring ret, and bringing fmt::Write to scope) and its epilogue (returning ret):

    let (intro_generics, fwd_generics, where_clause) = input.generics.split_for_impl();
    let StructName @ _ = &input.ident;

    Ok(quote_spanned!(Span::mixed_site()=>
        impl #intro_generics
            ::into_map::PrometheusAnnotations
        for
            #StructName #fwd_generics
        #where_clause
        {
            fn prometheus_annotations (self: &'_ Self)
              -> ::std::string::String
            {
                use ::core::fmt::Write;
                let mut ret = ::std::string::String::new();

                #function_body
  
                ret
            }
        }
    ))
4 Likes