tysh_derive/
lib.rs

1use proc_macro2::TokenStream;
2use quote::{quote, ToTokens};
3use syn::parse_macro_input;
4
5#[proc_macro_derive(TypeHash, attributes(type_hash))]
6/// Implement [`tysh::TypeHash`] trait for the struct.<br>
7/// Use `#[type_hash(name = "name")]` to specify the internal name.
8///
9/// # Example
10///
11/// ```rust
12/// use std::collections::hash_map::DefaultHasher;
13///
14/// # use tysh::TypeHash;
15/// #
16/// #[derive(TypeHash)]
17/// pub struct A {
18///     a: u8,
19///     b: u16,
20/// }
21///
22/// #[derive(TypeHash)]
23/// #[type_hash(name = "A")]
24/// pub struct B {
25///     #[type_hash(name = "a")]
26///     hoge: u8,
27///     #[type_hash(name = "b")]
28///     fuga: u16,
29/// }
30///
31/// assert_eq!(
32///     A::type_hash_one::<DefaultHasher>(),
33///     B::type_hash_one::<DefaultHasher>(),
34/// );
35/// ```
36///
37/// Enum is also supported.
38///
39/// ```rust
40/// # use tysh::TypeHash;
41///
42/// #[derive(TypeHash)]
43/// enum A {
44///     Hoge(String)
45///     Fuag { x: i32, y: i32 },
46///     Piyo,
47/// }
48/// ```
49pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
50    let input = parse_macro_input!(input as syn::DeriveInput);
51    let type_name = &input.ident;
52
53    let content = match &input.data {
54        syn::Data::Struct(v) => hashing_struct(&input, v),
55        syn::Data::Enum(v) => hashing_enum(&input, v),
56        syn::Data::Union(_) => panic!("Unions are not supported"),
57    };
58
59    let output = quote! {
60        impl ::tysh::TypeHash for #type_name {
61            fn type_hash<H: ::core::hash::Hasher>(hasher: &mut H) {
62                use ::core::hash::Hash;
63
64                #content
65            }
66        }
67    };
68
69    output.into()
70}
71
72fn hashing_struct(input: &syn::DeriveInput, structure: &syn::DataStruct) -> TokenStream {
73    let ident = parse_attrs(&input.attrs).unwrap_or(input.ident.to_string());
74    let fields = structure.fields.iter().map(hashing_field);
75
76    quote! {
77        "@struct@".hash(hasher);
78        #ident.hash(hasher);
79        #(#fields)*
80    }
81}
82
83fn hashing_enum(input: &syn::DeriveInput, enum_: &syn::DataEnum) -> TokenStream {
84    let ident = parse_attrs(&input.attrs).unwrap_or(input.ident.to_string());
85    let variants = enum_.variants.iter().map(hashing_variant);
86
87    quote! {
88        "@enum@".hash(hasher);
89        #ident.hash(hasher);
90        #(#variants)*
91    }
92}
93
94fn hashing_variant(variant: &syn::Variant) -> TokenStream {
95    let ident = parse_attrs(&variant.attrs).unwrap_or(variant.ident.to_string());
96    let fields = variant.fields.iter().map(hashing_field);
97
98    quote! {
99        #ident.hash(hasher);
100        #(#fields)*
101    }
102}
103
104fn hashing_field(field: &syn::Field) -> TokenStream {
105    let ident = parse_attrs(&field.attrs).unwrap_or(
106        field
107            .ident
108            .as_ref()
109            .map(|v| v.to_string())
110            .unwrap_or("@ano@".into()),
111    );
112    let ty = field.ty.to_token_stream();
113
114    quote! {
115        "@field@".hash(hasher);
116        #ident.hash(hasher);
117        <#ty as ::tysh::TypeHash>::type_hash(hasher);
118    }
119}
120
121fn parse_attrs(attrs: &[syn::Attribute]) -> Option<String> {
122    let attrs = attrs
123        .iter()
124        .filter(|at| at.path().is_ident("type_hash"))
125        .collect::<Vec<_>>();
126
127    if attrs.len() > 1 {
128        panic!("type_hash attribute can only be used once per field");
129    }
130
131    attrs.first().map(|v| match v.parse_args() {
132        Ok(syn::Meta::NameValue(m)) => {
133            if m.path.is_ident("name") {
134                let syn::Expr::Lit(lit) = m.value else {
135                    panic!("name attribute must be a string literal");
136                };
137                let syn::Lit::Str(lit) = lit.lit else {
138                    panic!("name attribute must be a string literal");
139                };
140                lit.value()
141            } else {
142                panic!("invalid type_hash attribute: expected `name = \"...\"`");
143            }
144        }
145        _ => {
146            panic!("invalid type_hash attribute")
147        }
148    })
149}