const_struct_version_derive/
lib.rs1use proc_macro::TokenStream;
4use quote::{ToTokens, quote};
5use syn::{
6 Attribute, Data, DeriveInput, Fields, GenericParam, parse_macro_input, spanned::Spanned,
7};
8
9#[proc_macro_derive(StructVersion)]
11pub fn derive_struct_version(input: TokenStream) -> TokenStream {
12 let input = parse_macro_input!(input as DeriveInput);
13 let ident = &input.ident;
14 let vis = &input.vis;
15
16 let (item_attrs, item_code) = match &input.data {
18 Data::Struct(data) => {
20 let fields = match &data.fields {
21 Fields::Named(fields) => Some(&fields.named),
22 Fields::Unnamed(unnamed) => Some(&unnamed.unnamed),
23 Fields::Unit => None,
24 };
25
26 match fields {
28 Some(fields) => {
29 let field_code = generate_struct_fields_code(&syn::FieldsNamed {
30 named: fields.clone(),
31 brace_token: Default::default(),
32 });
33 (process_attrs(&input.attrs), field_code)
34 }
35 None => (process_attrs(&input.attrs), proc_macro2::TokenStream::new()),
36 }
37 }
38 Data::Enum(data_enum) => (
40 process_attrs(&input.attrs),
41 generate_enum_variants_code(&data_enum.variants),
42 ),
43 Data::Union(_) => {
45 unimplemented!("Unions are not supported - you must implement StructVersion manually.")
46 }
47 };
48
49 let mut generics = input.generics.clone();
51 for param in &mut generics.params {
52 if let GenericParam::Type(type_param) = param {
53 type_param.bounds.push(syn::parse_quote!(StructVersion));
54 }
55 }
56 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
57
58 let version_impl = quote! {
60 #[doc(hidden)]
61 const _: () = {
62 extern crate const_struct_version as _const_struct_version;
63 use _const_struct_version::__private::sha1::Digest as _;
64
65 #[automatically_derived]
67 impl #impl_generics _const_struct_version::StructVersion for #ident #ty_generics #where_clause {
68 fn version() -> String {
69 let mut hasher = _const_struct_version::__private::sha1::Sha1::new();
70 #( _const_struct_version::__private::execute_if_serde_enabled(&mut hasher, |hasher| hasher.update(#item_attrs)); )*
71 #item_code
72 format!("{:x}", hasher.finalize())
73 }
74 }
75 };
76
77 impl #impl_generics #ident #ty_generics {
78 #vis fn version_cached() -> &'static str {
81 extern crate const_struct_version as _const_struct_version;
82 static VERSION: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
83 VERSION.get_or_init(|| <Self as _const_struct_version::StructVersion>::version())
84 }
85 }
86 };
87
88 version_impl.into()
89}
90
91fn generate_struct_fields_code(fields: &syn::FieldsNamed) -> proc_macro2::TokenStream {
93 let field_code = fields.named.iter().enumerate().map(|(index, field)| {
94 let field_name_str = field.ident.as_ref().map(|x| x.to_string()).unwrap_or(index.to_string());
95 let field_attrs = process_attrs(&field.attrs);
96 let field_ty = &field.ty;
97
98 quote! {
99 hasher.update(#field_name_str);
100 #( _const_struct_version::__private::execute_if_serde_enabled(&mut hasher, |hasher| hasher.update(#field_attrs)); )*
101 hasher.update(<#field_ty as _const_struct_version::StructVersion>::version().as_bytes());
102 }
103 });
104
105 quote! { #( #field_code )* }
106}
107
108fn generate_enum_variants_code(
110 variants: &syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
111) -> proc_macro2::TokenStream {
112 let variant_code = variants.iter().map(|variant| {
113 let variant_name = variant.ident.to_string();
114 let variant_attrs = process_attrs(&variant.attrs);
115
116 let disc_code = if let Some((eq_token, expr)) = &variant.discriminant {
118 let eq_ts = eq_token.to_token_stream();
119 let expr_ts = expr.to_token_stream();
120 let disc_str = format!("{}{}", eq_ts, expr_ts);
121 quote! { hasher.update(#disc_str); }
122 } else {
123 proc_macro2::TokenStream::new()
124 };
125
126 let fields_code = variant.fields.iter().enumerate().map(|(idx, field)| {
128 let field_name = match &field.ident {
129 Some(name) => name.to_string(),
130 None => idx.to_string(),
131 };
132 let field_attrs = process_attrs(&field.attrs);
133 let field_ty = &field.ty;
134
135 quote! {
136 hasher.update(#field_name);
137 #( _const_struct_version::__private::execute_if_serde_enabled(&mut hasher, |hasher| hasher.update(#field_attrs)); )*
138 hasher.update(<#field_ty as _const_struct_version::StructVersion>::version().as_bytes());
139 }
140 });
141
142 quote! {
143 hasher.update(#variant_name);
144 #( _const_struct_version::__private::execute_if_serde_enabled(&mut hasher, |hasher| hasher.update(#variant_attrs)); )*
145 #disc_code
146 #( #fields_code )*
147 }
148 });
149 quote! { #( #variant_code )* }
150}
151
152fn process_attrs(attrs: &[Attribute]) -> Vec<syn::LitStr> {
154 attrs
155 .iter()
156 .filter(|attr| attr.path().is_ident("serde"))
157 .map(|attr| {
158 let path = attr.path();
159 let tokens = quote! { #attr };
160 let attr_ts = quote! { #path #tokens };
161 let attr_str = attr_ts.to_string();
162 syn::LitStr::new(&attr_str, attr.span())
163 })
164 .collect()
165}