Jacob Birkett
Jacob Birkett

Reputation: 2135

Match lifetimes in "struct field syntax" passed to macro_rules

I have defined the following macro because I need many slightly different structs with Serde serialization.

macro_rules! instantiate {
    (
        $(#[$struct_meta:meta])*
        $struct_name:ident {
            $(
                $(#[$field_meta:meta])*
                $field_name:ident: $field_type:ty = $field_value:expr,
            )+
        }
    ) => {{
        $(#[$struct_meta])*
        struct $struct_name {
            $(
                $(#[$field_meta])*
                $field_name: $field_type,
            )*
        }

        $struct_name {
            $($field_name: $field_value,)*
        }
    }};
}

And it should be used just like this:

instantiate! {
    #[derive(serde::Serialize)]
    RequestParams {
        #[serde(serialize_with="serialize_debug")]
        ids: Vec<Base62Uint> = version_ids
            .into_iter()
            .map(std::fmt::Display::to_string)
            .collect(),
    }
}

However, I have another use for it:

let (hash, kind) = match hash {
    FileHashes {
        sha512: Some(hash), ..
    } => (hash, "sha512"),
    FileHashes {
        sha1: Some(hash), ..
    } => (hash, "sha1"),
    _ => todo!(),
};

instantiate! {
    #[derive(serde::Serialize)]
    RequestParams {
        algorithm: &str = kind,
    }
}

I end up with the following error, which was expected:

error[E0106]: missing lifetime specifier
   --> src/endpoints.rs:236:28
    |
236 |                 algorithm: &str = kind,
    |                            ^ expected named lifetime parameter
    |
help: consider introducing a named lifetime parameter
    |
88  ~         struct $struct_name<'a> {
89  |             $(
90  |                 $(#[$field_meta])*
91  |                 $field_name: $field_type,
92  |             )*
93  |         }
  ...

I want this macro to create a new lifetime for every $field_name, where the corresponding $field_type is a reference.

Here is what I have attempted:

$field_name: $(&$field_life:lifetime)? $field_type,

And stopped because I realized that would only capture the input lifetimes, which is less ergonomic than what I want, and I would end up with an ambiguity in the pattern because it input would be able to match either the &$field_life:lifetime portion or the $field_type:ty.

Upvotes: 0

Views: 365

Answers (2)

Chayim Friedman
Chayim Friedman

Reputation: 71605

macro_rules! cannot generate lifetimes, for the same reasons they cannot generate identifiers (Can a Rust macro create new identifiers?), because they cannot (without help) generate new token. You have to use a procedural macro for that.

You can use existing procedural macros that allow you to generate tokens, for example @dtolnay's seq-macro crate, but it will raise another problem: how do you know where to put the lifetime? You can (partially, because you cannot identify all types requiring lifetimes) solve that by tt-munching and identifying types that require lifetimes (for example references), but this will make your macro really complex, even more than a proc macro. I do not recommend doing that.

Upvotes: 1

Finomnis
Finomnis

Reputation: 22838

I assume that your minimal reproducible example is

macro_rules! instantiate {
    (
        $(#[$struct_meta:meta])*
        $struct_name:ident {
            $(
                $(#[$field_meta:meta])*
                $field_name:ident: $field_type:ty = $field_value:expr,
            )+
        }
    ) => {{
        $(#[$struct_meta])*
        struct $struct_name {
            $(
                $(#[$field_meta])*
                $field_name: $field_type,
            )*
        }

        $struct_name {
            $($field_name: $field_value,)*
        }
    }};
}

fn main() {
    let x = instantiate! {
        #[derive(serde::Serialize)]
        RequestParams {
            #[serde(default)]
            id: u32 = 10,
        }
    };

    println!("{}", serde_json::to_string(&x).unwrap());
}
{"id":10}

and your second example is, with the macro being identical:

fn main() {
    let (hash, kind) = (1234, "sha512");

    let x = instantiate! {
        #[derive(serde::Serialize)]
        RequestParams {
            algorithm: &str = kind,
        }
    };

    println!("{}", serde_json::to_string(&x).unwrap());
}
error[E0106]: missing lifetime specifier
  --> src/main.rs:31:24
   |
31 |             algorithm: &str = kind,
   |                        ^ expected named lifetime parameter
   |
help: consider introducing a named lifetime parameter
   |
12 ~         struct $struct_name<'a> {
13 |             $(
14 |                 $(#[$field_meta])*
15 |                 $field_name: $field_type,
16 |             )*
17 |         }
 ...

Solution #1 - the cheap solution without capturing lifetimes

fn main() {
    let (hash, kind) = (1234, "sha512");

    let x = instantiate! {
        #[derive(serde::Serialize)]
        RequestParams {
            algorithm: &'static str = kind,
        }
    };

    println!("{}", serde_json::to_string(&x).unwrap());
}
{"algorithm":"sha512"}

Solution #2 - capturing lifetimes

macro_rules! instantiate {
    (
        $(#[$struct_meta:meta])*
        $struct_name:ident $(< $($lifetimes:lifetime),* >)? {
            $(
                $(#[$field_meta:meta])*
                $field_name:ident: $field_type:ty = $field_value:expr,
            )+
        }
    ) => {{
        $(#[$struct_meta])*
        struct $struct_name $(< $($lifetimes),* >)? {
            $(
                $(#[$field_meta])*
                $field_name: $field_type,
            )*
        }

        $struct_name {
            $($field_name: $field_value,)*
        }
    }};
}

fn main() {
    let (hash, kind) = (1234, "sha512".to_string());

    let x = instantiate! {
        #[derive(serde::Serialize)]
        RequestParams<'a> {
            algorithm: &'a str = &kind,
        }
    };

    println!("{}", serde_json::to_string(&x).unwrap());
}
{"algorithm":"sha512"}

Upvotes: 1

Related Questions