const_struct_version_derive/
lib.rs

1// proc-macro crate: struct-version-derive
2
3use proc_macro::TokenStream;
4use quote::{ToTokens, quote};
5use syn::{
6    Attribute, Data, DeriveInput, Fields, GenericParam, parse_macro_input, spanned::Spanned,
7};
8
9/// Derive macro implementation for the `StructVersion` trait
10#[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    // Process item-level attributes and generate hash update code based on type
17    let (item_attrs, item_code) = match &input.data {
18        // Handle struct types
19        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            // Generate code for struct fields
27            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        // Handle enum types
39        Data::Enum(data_enum) => (
40            process_attrs(&input.attrs),
41            generate_enum_variants_code(&data_enum.variants),
42        ),
43        // Handle union types
44        Data::Union(_) => {
45            unimplemented!("Unions are not supported - you must implement StructVersion manually.")
46        }
47    };
48
49    // Add StructVersion bounds to all generic type parameters
50    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    // Generate final implementation code
59    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 implementation of StructVersion
66            #[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            /// Returns a cached version of the structure's hash
79            /// This is computed once and stored in a OnceLock for efficient access
80            #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
91/// Generate hash update code for struct fields
92fn 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
108/// Generate hash update code for enum variants (handles both named and unnamed fields)
109fn 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        // Handle discriminant (e.g., Variant = 1)
117        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        // Generate code for variant fields (both named and unnamed)
127        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
152/// Convert serde attributes to string literals for hashing
153fn 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}