Skip to main content

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            && let Ok(meta) = attr.parse_args_with(
55                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
56            )
57        {
58            for item in meta {
59                if let Meta::NameValue(MetaNameValue {
60                    path,
61                    value: Expr::Lit(expr_lit),
62                    ..
63                }) = item
64                {
65                    if path.is_ident("name") {
66                        if let Lit::Str(name) = expr_lit.lit {
67                            typename = quote! { #name };
68                        } else {
69                            return TokenStream::from(
70                                syn::Error::new_spanned(path, "invalid name").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    // Create a version of generics with Named bounds for the impl block
88    let mut generics_with_bounds = input.generics.clone();
89    if has_generics {
90        for param in generics_with_bounds.type_params_mut() {
91            param.bounds.push(syn::parse_quote!(typeuri::Named));
92        }
93    }
94    let (impl_generics_with_bounds, _, _) = generics_with_bounds.split_for_impl();
95
96    // Generate typename implementation based on whether we have generics
97    let (typename_impl, typehash_impl) = if has_generics {
98        // Create format string with placeholders for each generic parameter
99        let placeholders = vec!["{}"; type_params.len()].join(", ");
100        let placeholders_format_string = format!("<{}>", placeholders);
101        let format_string = quote! { concat!(std::module_path!(), "::", stringify!(#struct_name), #placeholders_format_string) };
102
103        let type_param_idents: Vec<_> = type_params.iter().map(|p| &p.ident).collect();
104        (
105            quote! {
106                typeuri::intern_typename!(Self, #format_string, #(#type_param_idents),*)
107            },
108            quote! {
109                typeuri::cityhasher::hash(Self::typename())
110            },
111        )
112    } else {
113        (
114            typename,
115            quote! {
116                static TYPEHASH: std::sync::LazyLock<u64> = std::sync::LazyLock::new(|| {
117                    typeuri::cityhasher::hash(<#struct_name as typeuri::Named>::typename())
118                });
119                *TYPEHASH
120            },
121        )
122    };
123
124    // Generate 'arm' for enums only.
125    let arm_impl = match &input.data {
126        Data::Enum(DataEnum { variants, .. }) => {
127            let match_arms = variants.iter().map(|v| {
128                let variant_name = &v.ident;
129                let variant_str = variant_name.to_string();
130                match &v.fields {
131                    Fields::Unit => quote! { Self::#variant_name => Some(#variant_str) },
132                    Fields::Unnamed(_) => quote! { Self::#variant_name(..) => Some(#variant_str) },
133                    Fields::Named(_) => quote! { Self::#variant_name { .. } => Some(#variant_str) },
134                }
135            });
136            quote! {
137                fn arm(&self) -> Option<&'static str> {
138                    match self {
139                        #(#match_arms,)*
140                    }
141                }
142            }
143        }
144        _ => quote! {},
145    };
146
147    let (_, ty_generics, where_clause) = input.generics.split_for_impl();
148
149    let expanded = quote! {
150        impl #impl_generics_with_bounds typeuri::Named for #struct_name #ty_generics #where_clause {
151            fn typename() -> &'static str { #typename_impl }
152            fn typehash() -> u64 { #typehash_impl }
153            #arm_impl
154        }
155    };
156
157    TokenStream::from(expanded)
158}