typeuri_macros/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Derive macros for the typeuri crate.
10
11use proc_macro::TokenStream;
12use quote::quote;
13use syn::Data;
14use syn::DataEnum;
15use syn::DeriveInput;
16use syn::Expr;
17use syn::Fields;
18use syn::Lit;
19use syn::Meta;
20use syn::MetaNameValue;
21use syn::parse_macro_input;
22
23/// Derive the [`typeuri::Named`] trait for a struct or enum.
24///
25/// The name of the type is its fully-qualified Rust path. The name may be
26/// overridden by providing a string value for the `name` attribute.
27///
28/// Example:
29/// ```ignore
30/// use typeuri_macros::Named;
31///
32/// #[derive(Named)]
33/// struct MyType;
34///
35/// #[derive(Named)]
36/// #[named(name = "custom::path::MyEnum")]
37/// enum MyEnum { A, B }
38/// ```
39#[proc_macro_derive(Named, attributes(named))]
40pub fn derive_named(input: TokenStream) -> TokenStream {
41    // Parse the input struct or enum
42    let input = parse_macro_input!(input as DeriveInput);
43    let struct_name = &input.ident;
44
45    let mut typename = quote! {
46        concat!(std::module_path!(), "::", stringify!(#struct_name))
47    };
48
49    let type_params: Vec<_> = input.generics.type_params().collect();
50    let has_generics = !type_params.is_empty();
51
52    for attr in &input.attrs {
53        if attr.path().is_ident("named") {
54            if let Ok(meta) = attr.parse_args_with(
55                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
56            ) {
57                for item in meta {
58                    if let Meta::NameValue(MetaNameValue {
59                        path,
60                        value: Expr::Lit(expr_lit),
61                        ..
62                    }) = item
63                    {
64                        if path.is_ident("name") {
65                            if let Lit::Str(name) = expr_lit.lit {
66                                typename = quote! { #name };
67                            } else {
68                                return TokenStream::from(
69                                    syn::Error::new_spanned(path, "invalid name")
70                                        .to_compile_error(),
71                                );
72                            }
73                        } else {
74                            return TokenStream::from(
75                                syn::Error::new_spanned(
76                                    path,
77                                    "unsupported attribute (only `name` is supported)",
78                                )
79                                .to_compile_error(),
80                            );
81                        }
82                    }
83                }
84            }
85        }
86    }
87
88    // Create a version of generics with Named bounds for the impl block
89    let mut generics_with_bounds = input.generics.clone();
90    if has_generics {
91        for param in generics_with_bounds.type_params_mut() {
92            param.bounds.push(syn::parse_quote!(typeuri::Named));
93        }
94    }
95    let (impl_generics_with_bounds, _, _) = generics_with_bounds.split_for_impl();
96
97    // Generate typename implementation based on whether we have generics
98    let (typename_impl, typehash_impl) = if has_generics {
99        // Create format string with placeholders for each generic parameter
100        let placeholders = vec!["{}"; type_params.len()].join(", ");
101        let placeholders_format_string = format!("<{}>", placeholders);
102        let format_string = quote! { concat!(std::module_path!(), "::", stringify!(#struct_name), #placeholders_format_string) };
103
104        let type_param_idents: Vec<_> = type_params.iter().map(|p| &p.ident).collect();
105        (
106            quote! {
107                typeuri::intern_typename!(Self, #format_string, #(#type_param_idents),*)
108            },
109            quote! {
110                typeuri::cityhasher::hash(Self::typename())
111            },
112        )
113    } else {
114        (
115            typename,
116            quote! {
117                static TYPEHASH: std::sync::LazyLock<u64> = std::sync::LazyLock::new(|| {
118                    typeuri::cityhasher::hash(<#struct_name as typeuri::Named>::typename())
119                });
120                *TYPEHASH
121            },
122        )
123    };
124
125    // Generate 'arm' for enums only.
126    let arm_impl = match &input.data {
127        Data::Enum(DataEnum { variants, .. }) => {
128            let match_arms = variants.iter().map(|v| {
129                let variant_name = &v.ident;
130                let variant_str = variant_name.to_string();
131                match &v.fields {
132                    Fields::Unit => quote! { Self::#variant_name => Some(#variant_str) },
133                    Fields::Unnamed(_) => quote! { Self::#variant_name(..) => Some(#variant_str) },
134                    Fields::Named(_) => quote! { Self::#variant_name { .. } => Some(#variant_str) },
135                }
136            });
137            quote! {
138                fn arm(&self) -> Option<&'static str> {
139                    match self {
140                        #(#match_arms,)*
141                    }
142                }
143            }
144        }
145        _ => quote! {},
146    };
147
148    let (_, ty_generics, where_clause) = input.generics.split_for_impl();
149
150    let expanded = quote! {
151        impl #impl_generics_with_bounds typeuri::Named for #struct_name #ty_generics #where_clause {
152            fn typename() -> &'static str { #typename_impl }
153            fn typehash() -> u64 { #typehash_impl }
154            #arm_impl
155        }
156    };
157
158    TokenStream::from(expanded)
159}