1use proc_macro2::TokenStream;
2use quote::{quote, ToTokens};
3use syn::parse_macro_input;
4
5#[proc_macro_derive(TypeHash, attributes(type_hash))]
6pub 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}